Merge pull request #2146 from pbiering/sharing-conv-bday-template
Sharing conv bday template support
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 3.7.5.dev
|
## 3.7.5.dev
|
||||||
|
* Add: [sharing] conversion_bday_summary_template (customize summary)
|
||||||
|
* Add: [sharing] conversion_bday_description_template (customize description)
|
||||||
|
* Add: [sharing] conversion_bday_alarm_trigger_template (customize alarms)
|
||||||
|
* Add: [sharing] conversion_bday_categories (customize)
|
||||||
|
* Add: [sharing] conversion_bday_age_max (limit in case of "age" placeholder is used which blocks using RRULE)
|
||||||
|
* Extension: [sharing/bday conversion]: add STATUS + CLASS fields
|
||||||
|
|
||||||
## 3.7.4
|
## 3.7.4
|
||||||
* Fix: sharing: PROPFIND returns now empty owner element in case of a mapped share as clients try PROPFIND on this not accessable href
|
* Fix: sharing: PROPFIND returns now empty owner element in case of a mapped share as clients try PROPFIND on this not accessable href
|
||||||
|
|||||||
@@ -2287,6 +2287,8 @@ Default: `true`
|
|||||||
|
|
||||||
##### default_permissions_create_token
|
##### default_permissions_create_token
|
||||||
|
|
||||||
|
_(>= 3.7.0)_
|
||||||
|
|
||||||
Default permissions for create token-based sharing
|
Default permissions for create token-based sharing
|
||||||
|
|
||||||
Default: `r`
|
Default: `r`
|
||||||
@@ -2295,12 +2297,77 @@ Supported: `rwEePp`
|
|||||||
|
|
||||||
##### default_permissions_create_map
|
##### default_permissions_create_map
|
||||||
|
|
||||||
|
_(>= 3.7.0)_
|
||||||
|
|
||||||
Default permissions for map-based sharing
|
Default permissions for map-based sharing
|
||||||
|
|
||||||
Default: `r`
|
Default: `r`
|
||||||
|
|
||||||
Supported: `rwEePp`
|
Supported: `rwEePp`
|
||||||
|
|
||||||
|
##### conversion_bday_summary_template
|
||||||
|
|
||||||
|
_(>= 3.7.5)_
|
||||||
|
|
||||||
|
Global template for summary of conversion "bday"
|
||||||
|
|
||||||
|
Default: `{{n:f} {n:g}|{fn}|{nickname}} ({year}) (BDAY)`
|
||||||
|
|
||||||
|
Supported placeholders (data used from VCARD)
|
||||||
|
* `{year}`: year of birthday (RFC6350#6.2.5)
|
||||||
|
* `{month}`: month of birthday (RFC6350#6.2.5)
|
||||||
|
* `{day}`: day of birthday (RFC6350#6.2.5)
|
||||||
|
* `{fn}`: full name (RFC6350#6.2.1)
|
||||||
|
* `{n:f}`: family name (RFC6350#6.2.2)
|
||||||
|
* `{n:g}`: given name (RFC6350#6.2.2)
|
||||||
|
* `{n:a}`: additional name (RFC6350#6.2.2)
|
||||||
|
* `{nickname}`: nick name (RFC6350#6.2.3)
|
||||||
|
|
||||||
|
Supported extra placeholders
|
||||||
|
* `{age}`: age, toggles to creation of single events instead using RRULE
|
||||||
|
|
||||||
|
Fallback is supported if placeholders inside `[...|...]` (first successful resolved one is used)
|
||||||
|
|
||||||
|
##### conversion_bday_description_template
|
||||||
|
|
||||||
|
_(>= 3.7.5)_
|
||||||
|
|
||||||
|
Global template for description of conversion "bday"
|
||||||
|
|
||||||
|
Default: `BDAY={year}-{month}-{day}`
|
||||||
|
|
||||||
|
Supported placeholders see `conversion_bday_summary_template`
|
||||||
|
|
||||||
|
##### conversion_bday_alarm_trigger_template
|
||||||
|
|
||||||
|
_(>= 3.7.5)_
|
||||||
|
|
||||||
|
Global template for alarm trigger of conversion "bday"
|
||||||
|
|
||||||
|
Default: ``
|
||||||
|
|
||||||
|
Supported format: `TIMEDELTA;DESCRIPTION` (separated by `|` if more alarms should be generated)
|
||||||
|
|
||||||
|
Supported format for `TIMEDELTA`: `[+-]?[0-9]+[WDHM]`
|
||||||
|
|
||||||
|
Supported placeholders for `DESCRIPTION` see `conversion_bday_summary_template`
|
||||||
|
|
||||||
|
##### conversion_bday_categories
|
||||||
|
|
||||||
|
_(>= 3.7.5)_
|
||||||
|
|
||||||
|
Global categories of conversion "bday", separated by `,`
|
||||||
|
|
||||||
|
Default: `Birthday`
|
||||||
|
|
||||||
|
##### conversion_bday_age_max
|
||||||
|
|
||||||
|
_(>= 3.7.5)_
|
||||||
|
|
||||||
|
Global max limit of "bday" age, only active in case of `{age}` is used as placeholder
|
||||||
|
|
||||||
|
Default: `99`
|
||||||
|
|
||||||
## Supported Clients
|
## Supported Clients
|
||||||
|
|
||||||
Radicale has been tested with:
|
Radicale has been tested with:
|
||||||
|
|||||||
19
SHARING.md
19
SHARING.md
@@ -43,7 +43,7 @@ Types of supported sharing configuration:
|
|||||||
* `TimestampCreated`: unixtime of creation
|
* `TimestampCreated`: unixtime of creation
|
||||||
* `TimestampUpdated`: unixtime of last update
|
* `TimestampUpdated`: unixtime of last update
|
||||||
* `Properties`: overlay properties (limited set whitelisted)
|
* `Properties`: overlay properties (limited set whitelisted)
|
||||||
* `Actions`: (reserved for future usage)
|
* `Actions`: specific configuration
|
||||||
|
|
||||||
`Enabled*`: _owner_ AND _user_ have to enable a share to become usable
|
`Enabled*`: _owner_ AND _user_ have to enable a share to become usable
|
||||||
|
|
||||||
@@ -246,6 +246,7 @@ Can be selected by `HTTP_ACCEPT` - default is equal to provided `CONTENT_TYPE`
|
|||||||
* `Enabled`: owner/user selected by authentication
|
* `Enabled`: owner/user selected by authentication
|
||||||
* `Hidden`: owner/user selected by authentication
|
* `Hidden`: owner/user selected by authentication
|
||||||
* `Properties`: properties to overlay
|
* `Properties`: properties to overlay
|
||||||
|
* `Actions`: specific configuration
|
||||||
|
|
||||||
### API Hooks
|
### API Hooks
|
||||||
|
|
||||||
@@ -315,7 +316,8 @@ Create a share by mapping a collection of an `Owner` to a token.
|
|||||||
| Permissions | str | optional (default:rp) |
|
| Permissions | str | optional (default:rp) |
|
||||||
| Enabled | bool | optional (owner/default:False) |
|
| Enabled | bool | optional (owner/default:False) |
|
||||||
| Hidden | bool | optional (owner/default:True) |
|
| Hidden | bool | optional (owner/default:True) |
|
||||||
| Properties | str | optional |
|
| Properties | str(dict) | optional |
|
||||||
|
| Actions | str(dict) | optional |
|
||||||
|
|
||||||
* Output: text/plain|application/json
|
* Output: text/plain|application/json
|
||||||
|
|
||||||
@@ -364,7 +366,8 @@ Create a share by mapping a collection of an `Owner` to an `User`.
|
|||||||
| Permissions | str | optional (default:r) |
|
| Permissions | str | optional (default:r) |
|
||||||
| Enabled | bool | optional (owner/default:False) |
|
| Enabled | bool | optional (owner/default:False) |
|
||||||
| Hidden | bool | optional (owner/default:True) |
|
| Hidden | bool | optional (owner/default:True) |
|
||||||
| Properties | optional |
|
| Properties | str(dict) | optional |
|
||||||
|
| Actions | str(dict) | optional |
|
||||||
|
|
||||||
* Output: text/plain|application/json
|
* Output: text/plain|application/json
|
||||||
|
|
||||||
@@ -518,7 +521,8 @@ Execute delete+create in case `PathOrToken` needs to be changed.
|
|||||||
| Permissions | str | adjust | optional | not-permitted |
|
| Permissions | str | adjust | optional | not-permitted |
|
||||||
| Enabled | bool | adjust | optional(owner) | optional(user) |
|
| Enabled | bool | adjust | optional(owner) | optional(user) |
|
||||||
| Hidden | bool | adjust | optional(owner) | optional(user) |
|
| Hidden | bool | adjust | optional(owner) | optional(user) |
|
||||||
| Properties | str | adjust | optional | optional |
|
| Properties | str(dict) | adjust | optional | optional |
|
||||||
|
| Actions | str(dict) | adjust | optional | not-permitted |
|
||||||
|
|
||||||
* Output: text/plain|application/json
|
* Output: text/plain|application/json
|
||||||
|
|
||||||
@@ -675,7 +679,6 @@ curl -u user:$userpw -d "$xml_pfc" -X PROPFIND http://localhost:5232/user/cal1-f
|
|||||||
|
|
||||||
Owner can create for itself or for particular user a virtual bday collection from an existing addressbook.
|
Owner can create for itself or for particular user a virtual bday collection from an existing addressbook.
|
||||||
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
Preconditions:
|
Preconditions:
|
||||||
@@ -687,6 +690,8 @@ Preconditions:
|
|||||||
|
|
||||||
#### Examples using API
|
#### Examples using API
|
||||||
|
|
||||||
|
##### Mapping
|
||||||
|
|
||||||
* Create as *map*
|
* Create as *map*
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -712,6 +717,8 @@ END:VCALENDAR
|
|||||||
|
|
||||||
Via WebUI an additional (virtual) calendar collection appears
|
Via WebUI an additional (virtual) calendar collection appears
|
||||||
|
|
||||||
|
##### Mapping as token
|
||||||
|
|
||||||
* Create as *token*
|
* Create as *token*
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -722,7 +729,7 @@ Status='success'
|
|||||||
PathOrToken='/.token/v1/lqqwqhZYTGi9uSPsixien_8G5jiSK0FfhNFRGG_t8UA0/'
|
PathOrToken='/.token/v1/lqqwqhZYTGi9uSPsixien_8G5jiSK0FfhNFRGG_t8UA0/'
|
||||||
```
|
```
|
||||||
|
|
||||||
* Fetch *map*
|
* Fetch *token*
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
## Fetch VCALENDAR auto-created from VADDRESSBOOK
|
## Fetch VCALENDAR auto-created from VADDRESSBOOK
|
||||||
|
|||||||
17
config
17
config
@@ -356,6 +356,23 @@
|
|||||||
# Supported: rwEePp
|
# Supported: rwEePp
|
||||||
#default_permissions_create_map = r
|
#default_permissions_create_map = r
|
||||||
|
|
||||||
|
# Global template for summary of conversion "bday"
|
||||||
|
#conversion_bday_summary_template = [{n:f} {n:g} |{fn}|{nickname}] ({year}) (BDAY)
|
||||||
|
|
||||||
|
# Global template for description of conversion "bday"
|
||||||
|
#conversion_bday_description_template = BDAY={year}-{month}-{day}
|
||||||
|
|
||||||
|
# Global template of alarm trigger of conversion "bday"
|
||||||
|
# Example: "-15H;BDAY tomorrow|9H;BDAY today"
|
||||||
|
#conversion_bday_alarm_trigger_template = ""
|
||||||
|
|
||||||
|
# Global categories of conversion "bday"
|
||||||
|
#conversion_bday_categories = Birthday
|
||||||
|
|
||||||
|
# Global max limit of "bday" age
|
||||||
|
# Only active in case of {age} is used as placeholder
|
||||||
|
#conversion_bday_age_max = 99
|
||||||
|
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
|
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ class ApplicationPartGet(ApplicationBase):
|
|||||||
if share and share['Conversion'] == "bday":
|
if share and share['Conversion'] == "bday":
|
||||||
if isinstance(item, storage.BaseCollection):
|
if isinstance(item, storage.BaseCollection):
|
||||||
# convert VCF to ICS
|
# convert VCF to ICS
|
||||||
answer = item.serialize(vcf_to_ics=True)
|
answer = item.serialize(vcf_to_ics=True, ShareActions=share['Actions'])
|
||||||
else:
|
else:
|
||||||
item_converted = item.convert_vcf_to_ics()
|
item_converted = item.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||||
if item_converted is not None:
|
if item_converted is not None:
|
||||||
answer = item_converted.serialize()
|
answer = item_converted.serialize()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ def xml_propfind_response(
|
|||||||
element.text = item.etag
|
element.text = item.etag
|
||||||
else:
|
else:
|
||||||
if share_bday_automap:
|
if share_bday_automap:
|
||||||
item_converted = item.convert_vcf_to_ics()
|
item_converted = item.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||||
if item_converted:
|
if item_converted:
|
||||||
element.text = item_converted.etag
|
element.text = item_converted.etag
|
||||||
else:
|
else:
|
||||||
@@ -382,14 +382,14 @@ def xml_propfind_response(
|
|||||||
logger.trace("PROPFIND/xml_propfind_response/getcontentlength: start bday automap handling for collection")
|
logger.trace("PROPFIND/xml_propfind_response/getcontentlength: start bday automap handling for collection")
|
||||||
length = 0
|
length = 0
|
||||||
for entry in item.get_all():
|
for entry in item.get_all():
|
||||||
item_ics = entry.convert_vcf_to_ics()
|
item_ics = entry.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||||
if item_ics is None:
|
if item_ics is None:
|
||||||
continue
|
continue
|
||||||
length += len(item_ics.vobject_item.serialize().encode(encoding))
|
length += len(item_ics.vobject_item.serialize().encode(encoding))
|
||||||
element.text = str(length)
|
element.text = str(length)
|
||||||
else:
|
else:
|
||||||
logger.trace("PROPFIND/xml_propfind_response/getcontentlength: start bday automap handling for single item")
|
logger.trace("PROPFIND/xml_propfind_response/getcontentlength: start bday automap handling for single item")
|
||||||
item_converted = item.convert_vcf_to_ics()
|
item_converted = item.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||||
if item_converted is not None:
|
if item_converted is not None:
|
||||||
element.text = str(len(item_converted.serialize()))
|
element.text = str(len(item_converted.serialize()))
|
||||||
else:
|
else:
|
||||||
@@ -457,7 +457,7 @@ def xml_propfind_response(
|
|||||||
logger.trace("PROPFIND/xml_propfind_response/getcontentcount: start bday automap handling")
|
logger.trace("PROPFIND/xml_propfind_response/getcontentcount: start bday automap handling")
|
||||||
items = []
|
items = []
|
||||||
for entry in item.get_all():
|
for entry in item.get_all():
|
||||||
item_ics = entry.convert_vcf_to_ics()
|
item_ics = entry.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||||
if item_ics is None:
|
if item_ics is None:
|
||||||
continue
|
continue
|
||||||
items.append(item_ics.vobject_item)
|
items.append(item_ics.vobject_item)
|
||||||
@@ -626,7 +626,7 @@ class ApplicationPartPropfind(ApplicationBase):
|
|||||||
for item, permission, raw_permissions in item_list:
|
for item, permission, raw_permissions in item_list:
|
||||||
if self._sharing._enabled and share:
|
if self._sharing._enabled and share:
|
||||||
if share['Conversion'] == "bday" and not isinstance(item, storage.BaseCollection):
|
if share['Conversion'] == "bday" and not isinstance(item, storage.BaseCollection):
|
||||||
if not item.convert_vcf_to_ics():
|
if not item.convert_vcf_to_ics(ShareActions=share['Actions']):
|
||||||
if len_item_list == 1:
|
if len_item_list == 1:
|
||||||
# only dedicated item requested
|
# only dedicated item requested
|
||||||
return httputils.NOT_FOUND
|
return httputils.NOT_FOUND
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
|
|||||||
# autoconvert
|
# autoconvert
|
||||||
retrieved_items_vcf_to_ics = []
|
retrieved_items_vcf_to_ics = []
|
||||||
for item, flag in retrieved_items:
|
for item, flag in retrieved_items:
|
||||||
item_ics = item.convert_vcf_to_ics()
|
item_ics = item.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||||
if item_ics is None:
|
if item_ics is None:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -593,7 +593,28 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
|||||||
("default_permissions_create_map", {
|
("default_permissions_create_map", {
|
||||||
"value": "r",
|
"value": "r",
|
||||||
"help": "default permissions for map-based sharing",
|
"help": "default permissions for map-based sharing",
|
||||||
"type": rights_permission})])),
|
"type": rights_permission}),
|
||||||
|
("conversion_bday_summary_template", {
|
||||||
|
"value": sharing.SHARING_BDAY_SUMMARY_TEMPLATE_DEFAULT,
|
||||||
|
"help": "conversion bday summary template",
|
||||||
|
"type": sharing.check_template_not_empty}),
|
||||||
|
("conversion_bday_description_template", {
|
||||||
|
"value": sharing.SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT,
|
||||||
|
"help": "conversion bday description template",
|
||||||
|
"type": sharing.check_template}),
|
||||||
|
("conversion_bday_alarm_trigger_template", {
|
||||||
|
"value": "",
|
||||||
|
"help": "conversion bday alarm trigger template",
|
||||||
|
"type": sharing.check_template_alarm_trigger}),
|
||||||
|
("conversion_bday_categories", {
|
||||||
|
"value": sharing.SHARING_BDAY_CATEGORIES_DEFAULT,
|
||||||
|
"help": "conversion bday categories",
|
||||||
|
"type": str}),
|
||||||
|
("conversion_bday_age_max", {
|
||||||
|
"value": str(sharing.SHARING_BDAY_AGE_MAX_DEFAULT),
|
||||||
|
"help": "conversion bday age max",
|
||||||
|
"type": sharing.check_bday_max_age}),
|
||||||
|
])),
|
||||||
("hook", OrderedDict([
|
("hook", OrderedDict([
|
||||||
("type", {
|
("type", {
|
||||||
"value": "none",
|
"value": "none",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from typing import (Any, Callable, List, MutableMapping, Optional, Sequence,
|
|||||||
import vobject
|
import vobject
|
||||||
|
|
||||||
from radicale import storage # noqa:F401
|
from radicale import storage # noqa:F401
|
||||||
from radicale import pathutils, utils
|
from radicale import pathutils, sharing, utils
|
||||||
from radicale.item import filter as radicale_filter
|
from radicale.item import filter as radicale_filter
|
||||||
from radicale.log import logger
|
from radicale.log import logger
|
||||||
|
|
||||||
@@ -47,6 +47,8 @@ PRODID_CONVERTED = u"-//Radicale//NONSGML " + utils.package_version("radicale")
|
|||||||
PRODID_SUFFIX = " (auto-converted by Radicale " + utils.package_version("radicale") + ")"
|
PRODID_SUFFIX = " (auto-converted by Radicale " + utils.package_version("radicale") + ")"
|
||||||
UID_SUFFIX = "-auto-converted-by-Radicale"
|
UID_SUFFIX = "-auto-converted-by-Radicale"
|
||||||
|
|
||||||
|
VCF_TO_ICS_SUPPORTED_PLACEHOLDERS: list = ["fn", "n:f", "n:g", "n:a", "age", "nickname", "year", "month", "day"]
|
||||||
|
|
||||||
|
|
||||||
def read_components(s: str) -> List[vobject.base.Component]:
|
def read_components(s: str) -> List[vobject.base.Component]:
|
||||||
"""Wrapper for vobject.readComponents"""
|
"""Wrapper for vobject.readComponents"""
|
||||||
@@ -362,6 +364,65 @@ def verify(file: str, encoding: str):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def replace_placeholders(text: str, placeholder_mapping: dict) -> str:
|
||||||
|
for placeholder in placeholder_mapping:
|
||||||
|
text = text.replace(placeholder, placeholder_mapping[placeholder])
|
||||||
|
|
||||||
|
# resolve {..|..} recursive
|
||||||
|
pattern = re.compile('(.*)(\\[)([^|]+)\\|(.+)(\\])(.*)')
|
||||||
|
logger.trace("item/convert_vcf_to_ics: resolve [..|..] starting with: %r", text)
|
||||||
|
while True:
|
||||||
|
match = pattern.match(text)
|
||||||
|
if not match:
|
||||||
|
# nothing more todo
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if match[3].startswith('!') and match[3].endswith('!'):
|
||||||
|
# not resolved variable
|
||||||
|
if '|' in match[4]:
|
||||||
|
# further recursion required
|
||||||
|
text = match[1] + match[2] + match[4] + match[5] + match[6]
|
||||||
|
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace/continue result: %r", text)
|
||||||
|
else:
|
||||||
|
text = match[1] + match[4] + match[6]
|
||||||
|
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace/final result: %r", text)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# resolved variable
|
||||||
|
text = match[1] + match[3] + match[6]
|
||||||
|
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace(resolved) result: %r", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_to_timedelta(trigger) -> Union[datetime.timedelta, None]:
|
||||||
|
# workaround as vobject is not supporting direct set of value
|
||||||
|
# limited implementatino of reverse function of timedeltaToString in vobject/icalendar.py
|
||||||
|
pattern = re.compile('([+-])?([0-9]+)([WDHM])$')
|
||||||
|
match = pattern.match(trigger)
|
||||||
|
if not match:
|
||||||
|
logger.error("item/convert_vcf_to_ics: trigger time value not valid: %r", trigger)
|
||||||
|
return None
|
||||||
|
|
||||||
|
sign = 1
|
||||||
|
if match[1] == "-":
|
||||||
|
sign = -1
|
||||||
|
|
||||||
|
value = int(match[2]) * sign
|
||||||
|
|
||||||
|
td: Union[datetime.timedelta, None] = None
|
||||||
|
|
||||||
|
if match[3] == "D":
|
||||||
|
td = datetime.timedelta(days=value)
|
||||||
|
elif match[3] == "M":
|
||||||
|
td = datetime.timedelta(minutes=value)
|
||||||
|
elif match[3] == "H":
|
||||||
|
td = datetime.timedelta(hours=value)
|
||||||
|
elif match[3] == "W":
|
||||||
|
td = datetime.timedelta(weeks=value)
|
||||||
|
|
||||||
|
return td
|
||||||
|
|
||||||
|
|
||||||
class Item:
|
class Item:
|
||||||
"""Class for address book and calendar entries."""
|
"""Class for address book and calendar entries."""
|
||||||
|
|
||||||
@@ -504,7 +565,8 @@ class Item:
|
|||||||
self.component_name
|
self.component_name
|
||||||
self._vobject_item = orig_vobject_item
|
self._vobject_item = orig_vobject_item
|
||||||
|
|
||||||
def convert_vcf_to_ics(self) -> Union["Item", None]:
|
def convert_vcf_to_ics(self, ShareActions: dict = {}) -> Union["Item", None]:
|
||||||
|
logger.trace("item/convert_vcf_to_ics: ShareActions: %r", ShareActions)
|
||||||
logger.trace("item/convert_vcf_to_ics: convert VCF to ICS (href): %r", self.href)
|
logger.trace("item/convert_vcf_to_ics: convert VCF to ICS (href): %r", self.href)
|
||||||
logger.trace("item/convert_vcf_to_ics: convert VCF to ICS (vobject): %r", self.vobject_item)
|
logger.trace("item/convert_vcf_to_ics: convert VCF to ICS (vobject): %r", self.vobject_item)
|
||||||
if self.vobject_item.name != "VCARD":
|
if self.vobject_item.name != "VCARD":
|
||||||
@@ -529,23 +591,54 @@ class Item:
|
|||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
placeholder_mapping: dict = {}
|
||||||
|
|
||||||
bdayS = match[1] + match[2] + match[3]
|
bdayS = match[1] + match[2] + match[3]
|
||||||
bdaySdesc = match[1] + "-" + match[2] + "-" + match[3]
|
|
||||||
bdayY = int(match[1])
|
bdayY = int(match[1])
|
||||||
bdayM = int(match[2])
|
bdayM = int(match[2])
|
||||||
bdayD = int(match[3])
|
bdayD = int(match[3])
|
||||||
|
|
||||||
|
placeholder_mapping['{year}'] = match[1]
|
||||||
|
placeholder_mapping['{month}'] = match[2]
|
||||||
|
placeholder_mapping['{day}'] = match[3]
|
||||||
|
|
||||||
# create ICS
|
# create ICS
|
||||||
if hasattr(self.vobject_item, "fn"):
|
if hasattr(self.vobject_item, "fn"):
|
||||||
name = self.vobject_item.fn.value
|
name = self.vobject_item.fn.value
|
||||||
elif hasattr(self.vobject_item, "n"):
|
elif hasattr(self.vobject_item, "n"):
|
||||||
name = self.vobject_item.n.value
|
name = self.vobject_item.n.value.family + " " + self.vobject_item.n.value.given
|
||||||
elif hasattr(self.vobject_item, "nickname"):
|
elif hasattr(self.vobject_item, "nickname"):
|
||||||
name = self.vobject_item.nickname.value
|
name = self.vobject_item.nickname.value
|
||||||
else:
|
else:
|
||||||
logger.trace("item/convert_vcf_to_ics: has bday but neither FN or N or NICKNAME (skip): %r", self.href)
|
logger.trace("item/convert_vcf_to_ics: has bday but neither FN or N or NICKNAME (skip): %r", self.href)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if hasattr(self.vobject_item, "nickname") and self.vobject_item.nickname.value != "":
|
||||||
|
placeholder_mapping['{nickname}'] = self.vobject_item.nickname.value
|
||||||
|
else:
|
||||||
|
placeholder_mapping['{nickname}'] = '!nickname!'
|
||||||
|
|
||||||
|
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!'
|
||||||
|
|
||||||
|
# rfc6350#6.2 FamilyName;GivenName;AdditionalNames;HonorificPrefixes;HonorificSuffixes
|
||||||
|
if hasattr(self.vobject_item, "n") and self.vobject_item.n.value.family != "":
|
||||||
|
placeholder_mapping['{n:f}'] = self.vobject_item.n.value.family
|
||||||
|
else:
|
||||||
|
placeholder_mapping['{n:f}'] = '!n:f!'
|
||||||
|
|
||||||
|
if hasattr(self.vobject_item, "n") and self.vobject_item.n.value.given != "":
|
||||||
|
placeholder_mapping['{n:g}'] = self.vobject_item.n.value.given
|
||||||
|
else:
|
||||||
|
placeholder_mapping['{n:g}'] = '!n:g!'
|
||||||
|
|
||||||
|
if hasattr(self.vobject_item, "n") and self.vobject_item.n.value.additional != "":
|
||||||
|
placeholder_mapping['{n:a}'] = self.vobject_item.n.value.additional
|
||||||
|
else:
|
||||||
|
placeholder_mapping['{n:a}'] = '!n:a!'
|
||||||
|
|
||||||
# create VCALENDAR
|
# create VCALENDAR
|
||||||
item_ics = vobject.newFromBehavior('vcalendar')
|
item_ics = vobject.newFromBehavior('vcalendar')
|
||||||
|
|
||||||
@@ -555,40 +648,134 @@ class Item:
|
|||||||
else:
|
else:
|
||||||
item_ics.add('prodid').value = PRODID_CONVERTED
|
item_ics.add('prodid').value = PRODID_CONVERTED
|
||||||
|
|
||||||
# create EVENT
|
# prepare SUMMARY
|
||||||
item_ics.add('vevent')
|
if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_summary_template' in ShareActions['config']:
|
||||||
|
summary = ShareActions['config']['conversion_bday_summary_template']
|
||||||
|
elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_summary_template' in ShareActions['config_default']:
|
||||||
|
summary = ShareActions['config_default']['conversion_bday_summary_template']
|
||||||
|
else:
|
||||||
|
summary = sharing.SHARING_BDAY_SUMMARY_TEMPLATE_DEFAULT # fallback
|
||||||
|
summary = replace_placeholders(summary, placeholder_mapping)
|
||||||
|
|
||||||
# set DTSTART
|
# prepare DESCRIPTION
|
||||||
dtstart = datetime.date(bdayY, bdayM, bdayD)
|
if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_description_template' in ShareActions['config']:
|
||||||
item_ics.vevent.add('dtstart').value = dtstart
|
description = ShareActions['config']['conversion_bday_description_template']
|
||||||
|
elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_description_template' in ShareActions['config_default']:
|
||||||
|
description = ShareActions['config_default']['conversion_bday_description_template']
|
||||||
|
else:
|
||||||
|
description = sharing.SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT # fallback
|
||||||
|
description = replace_placeholders(description, placeholder_mapping)
|
||||||
|
|
||||||
# calculate and set DTEND
|
# create CATEGORIES
|
||||||
dtend = dtstart + datetime.timedelta(days=1)
|
if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_categories' in ShareActions['config']:
|
||||||
item_ics.vevent.add('dtend').value = dtend
|
categories = ShareActions['config']['conversion_bday_categories'].split(',')
|
||||||
|
elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_categories' in ShareActions['config_default']:
|
||||||
|
categories = ShareActions['config_default']['conversion_bday_categories'].split(',')
|
||||||
|
else:
|
||||||
|
categories = sharing.SHARING_BDAY_CATEGORIES_DEFAULT.split(',') # fallback
|
||||||
|
|
||||||
# set UID
|
# check ALARM
|
||||||
|
if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_alarm_trigger_template' in ShareActions['config']:
|
||||||
|
alarm_trigger = ShareActions['config']['conversion_bday_alarm_trigger_template']
|
||||||
|
elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_alarm_trigger_template' in ShareActions['config_default']:
|
||||||
|
alarm_trigger = ShareActions['config_default']['conversion_bday_alarm_trigger_template']
|
||||||
|
else:
|
||||||
|
alarm_trigger = "" # default
|
||||||
|
|
||||||
|
vevent_enable_age = False
|
||||||
|
age_max = 0
|
||||||
|
if "{age}" in summary or "{age}" in description or "age" in alarm_trigger:
|
||||||
|
if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_age_max' in ShareActions['config']:
|
||||||
|
age_max = ShareActions['config']['conversion_bday_age_max']
|
||||||
|
elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_age_max' in ShareActions['config_default']:
|
||||||
|
age_max = ShareActions['config_default']['conversion_bday_age_max']
|
||||||
|
else:
|
||||||
|
age_max = sharing.SHARING_BDAY_AGE_MAX_DEFAULT # fallback
|
||||||
|
vevent_enable_age = True
|
||||||
|
|
||||||
|
# create UID
|
||||||
if hasattr(self.vobject_item, "uid"):
|
if hasattr(self.vobject_item, "uid"):
|
||||||
pattern = re.compile('^(.*)-[0-9a-fA-F]{12}(.*)$')
|
pattern = re.compile('^(.*)-[0-9a-fA-F]{12}(.*)$')
|
||||||
match = pattern.match(self.vobject_item.uid.value)
|
match = pattern.match(self.vobject_item.uid.value)
|
||||||
if match:
|
if match:
|
||||||
# replace part of UUID by bday
|
# replace part of UUID by bday
|
||||||
item_ics.vevent.add('uid').value = match[1] + '-' + 'bda0' + bdayS + match[2]
|
uid = match[1] + '-' + 'bda0' + bdayS + match[2]
|
||||||
else:
|
else:
|
||||||
item_ics.vevent.add('uid').value = self.vobject_item.uid.value + UID_SUFFIX
|
uid = self.vobject_item.uid.value + UID_SUFFIX
|
||||||
else:
|
else:
|
||||||
item_ics.vevent.add('uid').value = match[1] + match[2] + match[3] + "@" + name.replace(" ", "-") + UID_SUFFIX
|
uid = match[1] + match[2] + match[3] + "@" + name.replace(" ", "-") + UID_SUFFIX
|
||||||
|
|
||||||
# set SUMMARY
|
age = 0
|
||||||
item_ics.vevent.add('summary').value = name + " (BDAY)"
|
while age <= age_max:
|
||||||
|
# create EVENT
|
||||||
|
vevent = item_ics.add('vevent')
|
||||||
|
|
||||||
# set RRULE
|
# set DTSTART
|
||||||
item_ics.vevent.add('rrule').value = "FREQ=YEARLY"
|
if vevent_enable_age:
|
||||||
|
dtstart = datetime.date(bdayY + age, bdayM, bdayD)
|
||||||
|
else:
|
||||||
|
dtstart = datetime.date(bdayY, bdayM, bdayD)
|
||||||
|
vevent.add('dtstart').value = dtstart
|
||||||
|
|
||||||
# add transparency
|
# calculate and set DTEND
|
||||||
item_ics.vevent.add('transp').value = "TRANSPARENT"
|
dtend = dtstart + datetime.timedelta(days=1)
|
||||||
|
vevent.add('dtend').value = dtend
|
||||||
|
|
||||||
# add description
|
# set UID
|
||||||
item_ics.vevent.add('description').value = "BDAY=" + bdaySdesc
|
if vevent_enable_age:
|
||||||
|
uid_value = uid + "-AGE-" + str(age)
|
||||||
|
else:
|
||||||
|
uid_value = uid
|
||||||
|
vevent.add('uid').value = uid_value
|
||||||
|
|
||||||
|
# set SUMMARY
|
||||||
|
if vevent_enable_age:
|
||||||
|
summary_value = summary.replace("{age}", str(age))
|
||||||
|
else:
|
||||||
|
summary_value = summary
|
||||||
|
vevent.add('summary').value = summary_value
|
||||||
|
|
||||||
|
# set CATEGORIES
|
||||||
|
if categories is not None and categories != []:
|
||||||
|
vevent.add('categories').value = categories
|
||||||
|
|
||||||
|
# set CLASS
|
||||||
|
vevent.add('class').value = "PRIVATE"
|
||||||
|
|
||||||
|
# set STATUS
|
||||||
|
vevent.add('status').value = "CONFIRMED"
|
||||||
|
|
||||||
|
# set VALARM
|
||||||
|
if alarm_trigger is not None and alarm_trigger != "":
|
||||||
|
for entry in alarm_trigger.split('|'):
|
||||||
|
(trigger, alarm_description) = entry.split(';')
|
||||||
|
logger.trace("item/convert_vcf_to_ics: alarm trigger entry: %r (trigger=%r description=%r)", entry, trigger, description)
|
||||||
|
td = trigger_to_timedelta(trigger)
|
||||||
|
if td is not None:
|
||||||
|
alarm_description = replace_placeholders(alarm_description, placeholder_mapping)
|
||||||
|
alarm_description_value = alarm_description.replace("{age}", str(age))
|
||||||
|
valarm = vevent.add('valarm')
|
||||||
|
valarm.add('action').value = "DISPLAY"
|
||||||
|
valarm.add('description').value = alarm_description_value
|
||||||
|
valarm.add('trigger').value = td
|
||||||
|
|
||||||
|
# set RRULE
|
||||||
|
if not vevent_enable_age:
|
||||||
|
vevent.add('rrule').value = "FREQ=YEARLY"
|
||||||
|
|
||||||
|
# add transparency
|
||||||
|
vevent.add('transp').value = "TRANSPARENT"
|
||||||
|
|
||||||
|
# set DESCRIPTION
|
||||||
|
if vevent_enable_age:
|
||||||
|
description_value = description.replace("{age}", str(age))
|
||||||
|
else:
|
||||||
|
description_value = description
|
||||||
|
if description != "":
|
||||||
|
vevent.add('description').value = description_value
|
||||||
|
|
||||||
|
# increase age
|
||||||
|
age = age + 1
|
||||||
|
|
||||||
href = self.href
|
href = self.href
|
||||||
if href is not None:
|
if href is not None:
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import uuid
|
|||||||
from csv import DictWriter
|
from csv import DictWriter
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http import client
|
from http import client
|
||||||
from typing import Sequence, Union
|
from typing import Any, Sequence, Union
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
from radicale import (config, httputils, pathutils, rights, storage, types,
|
from radicale import (config, httputils, item, pathutils, rights, storage,
|
||||||
utils)
|
types, utils)
|
||||||
from radicale.log import logger
|
from radicale.log import logger
|
||||||
|
|
||||||
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
|
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
|
||||||
@@ -123,6 +123,77 @@ TOKEN_PATTERN_V1: str = "v1/[a-zA-Z0-9_\\-]{44}"
|
|||||||
|
|
||||||
OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname", "ICAL:calendar-order")
|
OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname", "ICAL:calendar-order")
|
||||||
|
|
||||||
|
SHARING_BDAY_AGE_MAX_LIMIT: int = 199 # maximum age to prevent unexpected DoS by config
|
||||||
|
SHARING_BDAY_AGE_MAX_DEFAULT: int = 99
|
||||||
|
SHARING_BDAY_SUMMARY_TEMPLATE_DEFAULT: str = "[{n:f} {n:g}|{fn}|{nickname}] ({year}) (BDAY)"
|
||||||
|
SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT: str = "BDAY={year}-{month}-{day}"
|
||||||
|
SHARING_BDAY_CATEGORIES_DEFAULT: str = 'Birthday'
|
||||||
|
|
||||||
|
|
||||||
|
def check_bday_max_age(data: Any) -> int:
|
||||||
|
value = int(data)
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("value is negative: %d" % value)
|
||||||
|
if value > SHARING_BDAY_AGE_MAX_LIMIT:
|
||||||
|
raise ValueError("value exceeds maximum (%d): %d" % (SHARING_BDAY_AGE_MAX_LIMIT, value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def check_template(data: Any) -> str:
|
||||||
|
placeholder_mapping: dict = {}
|
||||||
|
for placeholder in item.VCF_TO_ICS_SUPPORTED_PLACEHOLDERS:
|
||||||
|
placeholder_mapping["{" + placeholder + "}"] = '!' + placeholder + '!'
|
||||||
|
|
||||||
|
result = item.replace_placeholders(data, placeholder_mapping)
|
||||||
|
logger.trace("replace placeholders: %r -> %r", data, result)
|
||||||
|
pattern = re.compile('.*{.*}.*')
|
||||||
|
if pattern.search(result):
|
||||||
|
raise ValueError("template contains unsupported placeholder {..}: %r" % result)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def check_template_not_empty(data: Any) -> str:
|
||||||
|
result = check_template(data)
|
||||||
|
if result == "":
|
||||||
|
raise ValueError("template not allowed to be empty")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def check_template_alarm_trigger(data: Any) -> str:
|
||||||
|
if data is not None and data != '':
|
||||||
|
for entry in data.split('|'):
|
||||||
|
try:
|
||||||
|
(trigger, alarm_description) = entry.split(';')
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("alarm trigger template misses <trigger>;<alarm description>")
|
||||||
|
|
||||||
|
if trigger is not None and trigger != '':
|
||||||
|
td = item.trigger_to_timedelta(trigger)
|
||||||
|
if td is None:
|
||||||
|
raise ValueError("alarm trigger template contains unsupported trigger: %r" % trigger)
|
||||||
|
else:
|
||||||
|
raise ValueError("alarm trigger template misses trigger")
|
||||||
|
|
||||||
|
if alarm_description is not None and alarm_description != '':
|
||||||
|
try:
|
||||||
|
check_template_not_empty(alarm_description)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
raise ValueError("alarm trigger template misses description")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
ACTIONS_WHITELIST: dict = {
|
||||||
|
'config': {
|
||||||
|
'conversion_bday_summary_template': check_template_not_empty,
|
||||||
|
'conversion_bday_description_template': check_template,
|
||||||
|
'conversion_bday_alarm_trigger_template': check_template_alarm_trigger,
|
||||||
|
'conversion_bday_categories': str,
|
||||||
|
'conversion_bday_age_max': check_bday_max_age,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
CONVERSIONS_WHITELIST: Sequence[str] = ("bday", "none")
|
CONVERSIONS_WHITELIST: Sequence[str] = ("bday", "none")
|
||||||
|
|
||||||
|
|
||||||
@@ -166,6 +237,11 @@ class BaseSharing:
|
|||||||
self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map")
|
self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map")
|
||||||
self.permit_properties_overlay = configuration.get("sharing", "permit_properties_overlay")
|
self.permit_properties_overlay = configuration.get("sharing", "permit_properties_overlay")
|
||||||
self.enforce_properties_overlay = configuration.get("sharing", "enforce_properties_overlay")
|
self.enforce_properties_overlay = configuration.get("sharing", "enforce_properties_overlay")
|
||||||
|
self.conversion_bday_summary_template = configuration.get("sharing", "conversion_bday_summary_template")
|
||||||
|
self.conversion_bday_description_template = configuration.get("sharing", "conversion_bday_description_template")
|
||||||
|
self.conversion_bday_alarm_trigger_template = configuration.get("sharing", "conversion_bday_alarm_trigger_template")
|
||||||
|
self.conversion_bday_categories = configuration.get("sharing", "conversion_bday_categories")
|
||||||
|
self.conversion_bday_age_max = configuration.get("sharing", "conversion_bday_age_max")
|
||||||
|
|
||||||
logger.info("sharing.collection_by_map : %s", self.sharing_collection_by_map)
|
logger.info("sharing.collection_by_map : %s", self.sharing_collection_by_map)
|
||||||
logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token)
|
logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token)
|
||||||
@@ -175,6 +251,11 @@ class BaseSharing:
|
|||||||
logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map)
|
logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map)
|
||||||
logger.info("sharing.permit_properties_overlay: %s", self.permit_properties_overlay)
|
logger.info("sharing.permit_properties_overlay: %s", self.permit_properties_overlay)
|
||||||
logger.info("sharing.enforce_properties_overlay: %s", self.enforce_properties_overlay)
|
logger.info("sharing.enforce_properties_overlay: %s", self.enforce_properties_overlay)
|
||||||
|
logger.info("sharing.conversion_bday_summary_template: %r", self.conversion_bday_summary_template)
|
||||||
|
logger.info("sharing.conversion_bday_description_template: %r", self.conversion_bday_description_template)
|
||||||
|
logger.info("sharing.conversion_bday_alarm_trigger_template: %r", self.conversion_bday_alarm_trigger_template)
|
||||||
|
logger.info("sharing.conversion_bday_categories: %r", self.conversion_bday_categories)
|
||||||
|
logger.info("sharing.conversion_bday_age_max: %s", self.conversion_bday_age_max)
|
||||||
|
|
||||||
# database tasks
|
# database tasks
|
||||||
self.sharing_db_type = configuration.get("sharing", "type")
|
self.sharing_db_type = configuration.get("sharing", "type")
|
||||||
@@ -403,6 +484,73 @@ class BaseSharing:
|
|||||||
else:
|
else:
|
||||||
logger.trace("sharing/map: not active")
|
logger.trace("sharing/map: not active")
|
||||||
|
|
||||||
|
if share is not None:
|
||||||
|
if share['Conversion'] == "bday":
|
||||||
|
# autogenerate Actions if not existing
|
||||||
|
if share['Actions'] is None or 'config' not in share['Actions']:
|
||||||
|
share['Actions'] = {
|
||||||
|
'config_default': {
|
||||||
|
'conversion_bday_summary_template': self.conversion_bday_summary_template,
|
||||||
|
'conversion_bday_description_template': self.conversion_bday_description_template,
|
||||||
|
'conversion_bday_alarm_trigger_template': self.conversion_bday_alarm_trigger_template,
|
||||||
|
'conversion_bday_categories': self.conversion_bday_categories,
|
||||||
|
'conversion_bday_age_max': self.conversion_bday_age_max,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if 'config' in share['Actions']:
|
||||||
|
if 'conversion_bday_summary_template' in share['Actions']['config']:
|
||||||
|
# nothing to do
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if 'config_default' not in share['Actions']:
|
||||||
|
share['Actions'].update({'config_default': {}})
|
||||||
|
share['Actions']['config_default'].update(
|
||||||
|
{'conversion_bday_summary_template': self.conversion_bday_summary_template}
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'conversion_bday_description_template' in share['Actions']['config']:
|
||||||
|
# nothing to do
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if 'config_default' not in share['Actions']:
|
||||||
|
share['Actions'].update({'config_default': {}})
|
||||||
|
share['Actions']['config_default'].update(
|
||||||
|
{'conversion_bday_description_template': self.conversion_bday_description_template}
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'conversion_bday_alarm_trigger_template' in share['Actions']['config']:
|
||||||
|
# nothing to do
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if 'config_default' not in share['Actions']:
|
||||||
|
share['Actions'].update({'config_default': {}})
|
||||||
|
share['Actions']['config_default'].update(
|
||||||
|
{'conversion_bday_alarm_trigger_template': self.conversion_bday_alarm_trigger_template}
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'conversion_bday_categories' in share['Actions']['config']:
|
||||||
|
# nothing to do
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if 'config_default' not in share['Actions']:
|
||||||
|
share['Actions'].update({'config_default': {}})
|
||||||
|
share['Actions']['config_default'].update(
|
||||||
|
{'conversion_bday_categories': self.conversion_bday_categories}
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'conversion_bday_age_max' in share['Actions']['config']:
|
||||||
|
# nothing to do
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if 'config_default' not in share['Actions']:
|
||||||
|
share['Actions'].update({'config_default': {}})
|
||||||
|
share['Actions']['config_default'].update(
|
||||||
|
{'conversion_bday_age_max': self.conversion_bday_age_max}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r Actions=%r", share['ShareType'], share['PathOrToken'], share['PathMapped'], user, share['Owner'], share['Permissions'], share['Conversion'], share['Actions'])
|
||||||
|
|
||||||
return share
|
return share
|
||||||
|
|
||||||
# adjust a share
|
# adjust a share
|
||||||
@@ -789,7 +937,31 @@ class BaseSharing:
|
|||||||
return httputils.bad_request("Conversion not supported: %r" % Conversion)
|
return httputils.bad_request("Conversion not supported: %r" % Conversion)
|
||||||
|
|
||||||
if 'Actions' in request_data:
|
if 'Actions' in request_data:
|
||||||
return httputils.bad_request("Actions currently not supported (reserved for future needs)")
|
valid = True # default
|
||||||
|
hint = ""
|
||||||
|
for level1 in request_data['Actions']:
|
||||||
|
if level1 in ACTIONS_WHITELIST:
|
||||||
|
for level2 in request_data['Actions'][level1]:
|
||||||
|
if level2 in ACTIONS_WHITELIST[level1]:
|
||||||
|
if callable(ACTIONS_WHITELIST[level1][level2]):
|
||||||
|
try:
|
||||||
|
value = ACTIONS_WHITELIST[level1][level2](request_data['Actions'][level1][level2])
|
||||||
|
except ValueError:
|
||||||
|
hint = "'" + level1 + "': {'" + level2 + "'} is not supported"
|
||||||
|
valid = False
|
||||||
|
break
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
hint = "'" + level1 + "': {'" + level2 + "'} is not supported"
|
||||||
|
valid = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
hint = "'" + level1 + "' is not supported"
|
||||||
|
valid = False
|
||||||
|
break
|
||||||
|
if not valid:
|
||||||
|
return httputils.bad_request("Actions format not valid: " + hint)
|
||||||
|
Actions = request_data['Actions']
|
||||||
|
|
||||||
if 'Enabled' in request_data:
|
if 'Enabled' in request_data:
|
||||||
Enabled = request_data['Enabled']
|
Enabled = request_data['Enabled']
|
||||||
@@ -1112,7 +1284,8 @@ class BaseSharing:
|
|||||||
OwnerOrUser=user,
|
OwnerOrUser=user,
|
||||||
User=User,
|
User=User,
|
||||||
Timestamp=Timestamp,
|
Timestamp=Timestamp,
|
||||||
Properties=Properties)
|
Properties=Properties,
|
||||||
|
Actions=Actions)
|
||||||
else:
|
else:
|
||||||
result = self.database_update_sharing(
|
result = self.database_update_sharing(
|
||||||
ShareType=ShareType,
|
ShareType=ShareType,
|
||||||
@@ -1124,7 +1297,8 @@ class BaseSharing:
|
|||||||
OwnerOrUser=user,
|
OwnerOrUser=user,
|
||||||
User=User,
|
User=User,
|
||||||
Timestamp=Timestamp,
|
Timestamp=Timestamp,
|
||||||
Properties=Properties)
|
Properties=Properties,
|
||||||
|
Actions=Actions)
|
||||||
|
|
||||||
elif user == share['User']:
|
elif user == share['User']:
|
||||||
# User is only allowed to update Properties
|
# User is only allowed to update Properties
|
||||||
|
|||||||
@@ -441,6 +441,8 @@ class Sharing(sharing.BaseSharing):
|
|||||||
field = field.replace("{'", '{"') # replace for JSON start {' -> {"
|
field = field.replace("{'", '{"') # replace for JSON start {' -> {"
|
||||||
field = field.replace("'}", '"}') # replace for JSON end '} -> "}
|
field = field.replace("'}", '"}') # replace for JSON end '} -> "}
|
||||||
field = field.replace("': '", '": "') # replace for JSON entry/value ': ' -> ": "
|
field = field.replace("': '", '": "') # replace for JSON entry/value ': ' -> ": "
|
||||||
|
field = field.replace("': {", '": {') # replace for JSON entry/value ': { -> ": {
|
||||||
|
field = field.replace("': ", '": ') # replace for JSON entry/value ': -> ":(whitespace)
|
||||||
field = field.replace("', '", '", "') # replace for JSON delimiter ', ' -> ", "
|
field = field.replace("', '", '", "') # replace for JSON delimiter ', ' -> ", "
|
||||||
logger.trace("json prep quote replacer match (after) : %s", field)
|
logger.trace("json prep quote replacer match (after) : %s", field)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class BaseCollection:
|
|||||||
"""Get the HTTP-datetime of when the collection was modified."""
|
"""Get the HTTP-datetime of when the collection was modified."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def serialize(self, vcf_to_ics: bool = False) -> str:
|
def serialize(self, vcf_to_ics: bool = False, ShareActions: dict = {}) -> str:
|
||||||
"""Get the unicode string representing the whole collection."""
|
"""Get the unicode string representing the whole collection."""
|
||||||
if self.tag == "VCALENDAR":
|
if self.tag == "VCALENDAR":
|
||||||
in_vcalendar = False
|
in_vcalendar = False
|
||||||
@@ -288,7 +288,7 @@ class BaseCollection:
|
|||||||
items = []
|
items = []
|
||||||
for item in self.get_all():
|
for item in self.get_all():
|
||||||
logger.trace("storage/convert VCF to ICS: %r:", item)
|
logger.trace("storage/convert VCF to ICS: %r:", item)
|
||||||
item_ics = item.convert_vcf_to_ics()
|
item_ics = item.convert_vcf_to_ics(ShareActions=ShareActions)
|
||||||
if item_ics is None:
|
if item_ics is None:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ BEGIN:VCARD
|
|||||||
VERSION:3.0
|
VERSION:3.0
|
||||||
PRODID:+//IDN bitfire.at//DAVx5/3.3.5-ose ez-vcard/0.11.0
|
PRODID:+//IDN bitfire.at//DAVx5/3.3.5-ose ez-vcard/0.11.0
|
||||||
UID:contact2-with-bday
|
UID:contact2-with-bday
|
||||||
N:Test;N;;;
|
N:FamilyTest;GivenTest;AdditionalsTest;;
|
||||||
FN:Test-FN
|
FN:Test-FN
|
||||||
NICKNAME:Test-NICKNAME
|
NICKNAME:Test-NICKNAME
|
||||||
BDAY:1970-01-01
|
BDAY:1970-01-01
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ BEGIN:VCARD
|
|||||||
VERSION:3.0
|
VERSION:3.0
|
||||||
PRODID:+//Manual/0.0.1
|
PRODID:+//Manual/0.0.1
|
||||||
UID:05cf4901-e581-44ee-bb0a-1e1a875da44a
|
UID:05cf4901-e581-44ee-bb0a-1e1a875da44a
|
||||||
N:Test;N;C3;;
|
N:Family3Test;Given3Test;;;
|
||||||
FN:Test-FN-C3
|
FN:Test-FN-C3
|
||||||
NICKNAME:Test-NICKNAME-C3
|
|
||||||
BDAY:1990-01-01
|
BDAY:1990-01-01
|
||||||
END:VCARD
|
END:VCARD
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ class TestBaseAuthRequests(BaseTest):
|
|||||||
delay = .3
|
delay = .3
|
||||||
delay_min = delay * 0.9 # no random jitter during test
|
delay_min = delay * 0.9 # no random jitter during test
|
||||||
delay_max = delay + 0.2 # no random jitter during test
|
delay_max = delay + 0.2 # no random jitter during test
|
||||||
if sys.platform == "darwin": # no reliable sleep times
|
if sys.platform == "darwin" or sys.platform == 'win32': # no reliable sleep times
|
||||||
delay_max = delay_max * 1.5
|
delay_max = delay_max * 1.5
|
||||||
|
|
||||||
time_begin = datetime.datetime.now()
|
time_begin = datetime.datetime.now()
|
||||||
|
|||||||
@@ -4830,6 +4830,7 @@ permissions: RrWw""")
|
|||||||
assert "DTEND;VALUE=DATE:19700102" in answer
|
assert "DTEND;VALUE=DATE:19700102" in answer
|
||||||
assert "TRANSP:TRANSPARENT" in answer
|
assert "TRANSP:TRANSPARENT" in answer
|
||||||
assert "DESCRIPTION:BDAY=1970-01-01" in answer
|
assert "DESCRIPTION:BDAY=1970-01-01" in answer
|
||||||
|
assert "CATEGORIES:Birthday" in answer
|
||||||
# content type must be adjusted
|
# content type must be adjusted
|
||||||
assert 'Content-Type' in headers
|
assert 'Content-Type' in headers
|
||||||
assert 'text/calendar' in headers['Content-Type']
|
assert 'text/calendar' in headers['Content-Type']
|
||||||
@@ -4879,6 +4880,9 @@ permissions: RrWw""")
|
|||||||
assert "DTEND;VALUE=DATE:19700102" in answer
|
assert "DTEND;VALUE=DATE:19700102" in answer
|
||||||
assert "TRANSP:TRANSPARENT" in answer
|
assert "TRANSP:TRANSPARENT" in answer
|
||||||
assert "DESCRIPTION:BDAY=1970-01-01" in answer
|
assert "DESCRIPTION:BDAY=1970-01-01" in answer
|
||||||
|
assert "CATEGORIES:Birthday" in answer
|
||||||
|
assert "CLASS:PRIVATE" in answer
|
||||||
|
assert "STATUS:CONFIRMED" in answer
|
||||||
# content type must be adjusted
|
# content type must be adjusted
|
||||||
assert 'Content-Type' in headers
|
assert 'Content-Type' in headers
|
||||||
assert 'text/calendar' in headers['Content-Type']
|
assert 'text/calendar' in headers['Content-Type']
|
||||||
@@ -4931,6 +4935,635 @@ permissions: RrWw""")
|
|||||||
status, prop = response["D:getcontenttype"]
|
status, prop = response["D:getcontenttype"]
|
||||||
assert "text/calendar" in str(prop.text)
|
assert "text/calendar" in str(prop.text)
|
||||||
|
|
||||||
|
def test_sharing_api_map_vcf_bday_template(self) -> None:
|
||||||
|
"""share-by-map with conversion=bday template tests."""
|
||||||
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
|
"htpasswd_filename": self.htpasswd_file_path,
|
||||||
|
"htpasswd_encryption": "plain"},
|
||||||
|
"sharing": {
|
||||||
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_properties_overlay": "True",
|
||||||
|
"enforce_properties_overlay": "True",
|
||||||
|
"collection_by_map": "True"},
|
||||||
|
"logging": {"request_header_on_debug": "False",
|
||||||
|
"response_content_on_debug": "True",
|
||||||
|
"response_header_on_debug": "True",
|
||||||
|
"request_content_on_debug": "True"},
|
||||||
|
"rights": {"type": "owner_only"}})
|
||||||
|
|
||||||
|
json_dict: dict
|
||||||
|
|
||||||
|
logging.info("\n*** prepare and test access")
|
||||||
|
|
||||||
|
for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)):
|
||||||
|
logging.info("\n*** test: %s", db_type)
|
||||||
|
self.configure({"sharing": {"type": db_type}})
|
||||||
|
|
||||||
|
path_mapped = "/owner/adressbook-" + db_type + ".vcf/"
|
||||||
|
path_shared_r = "/user/calendar-bday-abook-shared-by-owner-r-" + db_type + ".ics/"
|
||||||
|
self.create_addressbook(path_mapped, login="owner:ownerpw")
|
||||||
|
|
||||||
|
contact2 = get_file_content("contact2-with-bday.vcf")
|
||||||
|
path2 = path_mapped + "/contact2-with-bday.vcf"
|
||||||
|
path_shared_2 = path_shared_r + "/contact2-with-bday.vcf"
|
||||||
|
self.put(path2, contact2, login="owner:ownerpw")
|
||||||
|
|
||||||
|
contact3 = get_file_content("contact3-with-bday.vcf")
|
||||||
|
path3 = path_mapped + "/contact3-with-bday.vcf"
|
||||||
|
path_shared_3 = path_shared_r + "/contact3-with-bday.vcf"
|
||||||
|
self.put(path3, contact3, login="owner:ownerpw")
|
||||||
|
|
||||||
|
# create map
|
||||||
|
logging.info("\n*** create map(bday) user/owner:r -> ok")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Conversion'] = "bday"
|
||||||
|
json_dict['Permissions'] = "rP"
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Hidden'] = False
|
||||||
|
json_dict['Properties'] = {"D:displayname": "Test-BDAY"}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
answer_dict = json.loads(answer)
|
||||||
|
assert answer_dict['Status'] == "success"
|
||||||
|
|
||||||
|
# enable map by user
|
||||||
|
logging.info("\n*** enable map(bday) by user")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] (BDAY)",
|
||||||
|
"conversion_bday_description_template": "BDAY={year}-{month}-{day}",
|
||||||
|
"conversion_bday_alarm_trigger_template": "-15H;BDAY tomorrow|9H;BDAY today",
|
||||||
|
}})
|
||||||
|
|
||||||
|
# verify content as user
|
||||||
|
logging.info("\n*** GET collection user format:default -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format with fn+Birthday -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (Birthday)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "[{n:f} {n:g} {n:a}|{fn}|{nickname}] (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format:n -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:FamilyTest GivenTest AdditionalsTest (Birthday)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "[{nickname}|{n:f} {n:g} {n:a}|{fn}] (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format:nickname -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-NICKNAME (Birthday)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "{nickname} (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format:nickname not resolvable -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "SUMMARY:!nickname! (Birthday)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "[{nickname}|{nickname}|{fn}] (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format:nickname with fn fallback -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN-C3 (Birthday)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "[{nickname}|{nickname}|{n:g} {n:f}] (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format:nickname with n fallback -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "SUMMARY:Given3Test Family3Test (Birthday)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "[{nickname}|{nickname}|{n:f}, {n:g}] (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format:nickname with n fallback -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "SUMMARY:Family3Test\\, Given3Test (Birthday)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_summary_template": "[{nickname}|{nickname}|{n:f} {n:g} {n:a}] (Birthday)"}})
|
||||||
|
logging.info("\n*** GET collection user format:nickname with n fallback -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "SUMMARY:Family3Test Given3Test !n:a! (Birthday)" in answer
|
||||||
|
assert "DESCRIPTION:BDAY=1990-01-01" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_description_template": "Birthday={year}{month}{day}"}})
|
||||||
|
logging.info("\n*** GET collection user format: description -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "DESCRIPTION:Birthday=19900101" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_description_template": "year={year} month={month} day={day}\nfn='{fn}'\nn:g='{n:g}'\nn:f='{n:f}'\nn:a='{n:a}'"}})
|
||||||
|
logging.info("\n*** GET collection user format: description -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "DESCRIPTION:year=1990 month=01 day=01" in answer
|
||||||
|
assert "DESCRIPTION:BDAY tomorrow" in answer
|
||||||
|
assert "DESCRIPTION:BDAY today" in answer
|
||||||
|
assert "TRIGGER:-PT15H" in answer
|
||||||
|
assert "TRIGGER:PT9H" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_alarm_trigger_template": "-12H;Birthday tomorrow of {fn}|12H;Birthday today of {n:g} {n:f}"}})
|
||||||
|
logging.info("\n*** GET collection user format: description -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "DESCRIPTION:Birthday tomorrow of Test-FN-C3" in answer
|
||||||
|
assert "DESCRIPTION:Birthday today of Given3Test Family3Test" in answer
|
||||||
|
assert "TRIGGER:-PT12H" in answer
|
||||||
|
assert "TRIGGER:PT12H" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {"conversion_bday_categories": "Birthday,Geburtstag"}})
|
||||||
|
logging.info("\n*** GET collection user format: description -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "DESCRIPTION:Birthday tomorrow of Test-FN-C3" in answer
|
||||||
|
assert "DESCRIPTION:Birthday today of Given3Test Family3Test" in answer
|
||||||
|
assert "CATEGORIES:Birthday,Geburtstag" in answer
|
||||||
|
|
||||||
|
self.configure({
|
||||||
|
"sharing": {"conversion_bday_description_template": "",
|
||||||
|
"conversion_bday_alarm_trigger_template": "",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
logging.info("\n*** GET collection user format: no description -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "DESCRIPTION" not in answer
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_summary_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_description_template": "year={year} month={month} day={day}\nfn='{fn}'\nn:g='{n:g}'\nn:f='{n:f}'\nn:a='{notsupported}'",
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_description_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "[{nickname}|{nickname}|{n:f} {n:g} {n:a}] {notsupportedplaceholder}",
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_summary_template empty")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "",
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_description_template empty")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_description_template": "",
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_alarm_trigger_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_alarm_trigger_template": "-12T;Birthday tomorrow of {fn}"
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_alarm_trigger_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_alarm_trigger_template": "BROKEN;Birthday tomorrow of {fn}"
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_alarm_trigger_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_alarm_trigger_template": "-+BROKEN;Birthday tomorrow of {fn}"
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_alarm_trigger_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_alarm_trigger_template": "+0;Birthday tomorrow of {fn}"
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_alarm_trigger_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_alarm_trigger_template": "-12H"
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_alarm_trigger_template not supported")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_alarm_trigger_template": "-12H;"
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# update template
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r with invalid config -> 400")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_alarm_trigger_template": "-12H;"
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
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",
|
||||||
|
"htpasswd_filename": self.htpasswd_file_path,
|
||||||
|
"htpasswd_encryption": "plain"},
|
||||||
|
"sharing": {
|
||||||
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_properties_overlay": "True",
|
||||||
|
"enforce_properties_overlay": "True",
|
||||||
|
"collection_by_map": "True"},
|
||||||
|
"logging": {"request_header_on_debug": "False",
|
||||||
|
"response_content_on_debug": "True",
|
||||||
|
"response_header_on_debug": "True",
|
||||||
|
"request_content_on_debug": "True"},
|
||||||
|
"rights": {"type": "owner_only"}})
|
||||||
|
|
||||||
|
json_dict: dict
|
||||||
|
|
||||||
|
logging.info("\n*** prepare and test access")
|
||||||
|
|
||||||
|
for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)):
|
||||||
|
logging.info("\n*** test: %s", db_type)
|
||||||
|
self.configure({"sharing": {"type": db_type}})
|
||||||
|
|
||||||
|
path_mapped = "/owner/adressbook-" + db_type + ".vcf/"
|
||||||
|
path_shared_r = "/user/calendar-bday-abook-shared-by-owner-r-" + db_type + ".ics/"
|
||||||
|
self.create_addressbook(path_mapped, login="owner:ownerpw")
|
||||||
|
|
||||||
|
contact2 = get_file_content("contact2-with-bday.vcf")
|
||||||
|
path2 = path_mapped + "/contact2-with-bday.vcf"
|
||||||
|
path_shared_2 = path_shared_r + "/contact2-with-bday.vcf"
|
||||||
|
self.put(path2, contact2, login="owner:ownerpw")
|
||||||
|
|
||||||
|
# create map
|
||||||
|
logging.info("\n*** create map(bday) user/owner:r -> ok")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Conversion'] = "bday"
|
||||||
|
json_dict['Permissions'] = "rP"
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Hidden'] = False
|
||||||
|
json_dict['Properties'] = {"D:displayname": "Test-BDAY"}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
answer_dict = json.loads(answer)
|
||||||
|
assert answer_dict['Status'] == "success"
|
||||||
|
|
||||||
|
# enable map by user
|
||||||
|
logging.info("\n*** enable map(bday) by user")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] (BDAY)",
|
||||||
|
"conversion_bday_description_template": "BDAY={year}-{month}-{day}",
|
||||||
|
}})
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_age_max < 0")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_age_max": -1,
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logging.info("\n*** configuration test: conversion_bday_age_max > MAX")
|
||||||
|
try:
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_age_max": (sharing.SHARING_BDAY_AGE_MAX_LIMIT + 1),
|
||||||
|
}})
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# verify content as user
|
||||||
|
logging.info("\n*** GET collection user format:default -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY)" in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] ({age}. Birthday)",
|
||||||
|
}})
|
||||||
|
logging.info("\n*** GET collection user format:text -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (0. Birthday)" in answer
|
||||||
|
assert "SUMMARY:Test-FN (1. Birthday)" in answer
|
||||||
|
assert "SUMMARY:Test-FN (99. Birthday)" in answer
|
||||||
|
assert "SUMMARY:Test-FN (100. Birthday)" not in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] (BDAY)",
|
||||||
|
"conversion_bday_description_template": "AGE={age}",
|
||||||
|
}})
|
||||||
|
logging.info("\n*** GET collection user format:text -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY)" in answer
|
||||||
|
assert "DESCRIPTION:AGE=0" in answer
|
||||||
|
assert "DESCRIPTION:AGE=1" in answer
|
||||||
|
assert "DESCRIPTION:AGE=99" in answer
|
||||||
|
assert "DESCRIPTION:AGE=100" not in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] (BDAY)",
|
||||||
|
"conversion_bday_description_template": "BDAY={year}-{month}-{day}",
|
||||||
|
"conversion_bday_alarm_trigger_template": "-15H;alarm {fn} {age}. birthday",
|
||||||
|
}})
|
||||||
|
logging.info("\n*** GET collection user format:text -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY)" in answer
|
||||||
|
assert "DESCRIPTION:BDAY=1970-01-01" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 0. birthday" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 1. birthday" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 99. birthday" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 100. birthday" not in answer
|
||||||
|
|
||||||
|
# update template
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> ok")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_age_max": 5,
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
answer_dict = json.loads(answer)
|
||||||
|
assert answer_dict['Status'] == "success"
|
||||||
|
|
||||||
|
logging.info("\n*** GET collection user format:text -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY)" in answer
|
||||||
|
assert "DESCRIPTION:BDAY=1970-01-01" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 0. birthday" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 1. birthday" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 5. birthday" in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 6. birthday" not in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 99. birthday" not in answer
|
||||||
|
assert "DESCRIPTION:alarm Test-FN 100. birthday" not in answer
|
||||||
|
|
||||||
|
self.configure({"sharing": {
|
||||||
|
"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] (BDAY)",
|
||||||
|
"conversion_bday_description_template": "AGE={age}",
|
||||||
|
"conversion_bday_alarm_trigger_template": "",
|
||||||
|
}})
|
||||||
|
logging.info("\n*** GET collection user limit age to 5 -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY)" in answer
|
||||||
|
assert "DESCRIPTION:AGE=0" in answer
|
||||||
|
assert "DESCRIPTION:AGE=1" in answer
|
||||||
|
assert "DESCRIPTION:AGE=5" in answer
|
||||||
|
assert "DESCRIPTION:AGE=6" not in answer
|
||||||
|
assert "DESCRIPTION:AGE=99" not in answer
|
||||||
|
assert "DESCRIPTION:AGE=100" not in answer
|
||||||
|
|
||||||
|
# update template with invalid data test
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> age_max not negative")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_age_max": -1,
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
# update template with invalid data test
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> age_max exceeds MAX")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_age_max": (sharing.SHARING_BDAY_AGE_MAX_LIMIT + 1),
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
# update template with valid data test
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> age_max ok")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_age_max": 0,
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
# update template with valid data test
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> age_max < MAX")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_age_max": sharing.SHARING_BDAY_AGE_MAX_LIMIT,
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> unsupported level 1")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"level1": {
|
||||||
|
"level2": "test",
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> unsupported level 2")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped
|
||||||
|
json_dict['PathOrToken'] = path_shared_r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"level2": "test",
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
def test_sharing_api_map_vcf_bday_per_share_template(self) -> None:
|
||||||
|
"""share-by-map with conversion=bday template per share tests."""
|
||||||
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
|
"htpasswd_filename": self.htpasswd_file_path,
|
||||||
|
"htpasswd_encryption": "plain"},
|
||||||
|
"sharing": {
|
||||||
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_properties_overlay": "True",
|
||||||
|
"enforce_properties_overlay": "True",
|
||||||
|
"collection_by_map": "True"},
|
||||||
|
"logging": {"request_header_on_debug": "False",
|
||||||
|
"response_content_on_debug": "True",
|
||||||
|
"response_header_on_debug": "True",
|
||||||
|
"request_content_on_debug": "True"},
|
||||||
|
"rights": {"type": "owner_only"}})
|
||||||
|
|
||||||
|
json_dict: dict
|
||||||
|
|
||||||
|
logging.info("\n*** prepare and test access")
|
||||||
|
|
||||||
|
for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)):
|
||||||
|
logging.info("\n*** test: %s", db_type)
|
||||||
|
self.configure({"sharing": {"type": db_type}})
|
||||||
|
|
||||||
|
path_mapped_2 = "/owner/adressbook2-" + db_type + ".vcf/"
|
||||||
|
path_mapped_3 = "/owner/adressbook3-" + db_type + ".vcf/"
|
||||||
|
|
||||||
|
path_shared_2r = "/user/calendar-bday-abook2-shared-by-owner-r-" + db_type + ".ics/"
|
||||||
|
path_shared_3r = "/user/calendar-bday-abook3-shared-by-owner-r-" + db_type + ".ics/"
|
||||||
|
|
||||||
|
self.create_addressbook(path_mapped_2, login="owner:ownerpw")
|
||||||
|
self.create_addressbook(path_mapped_3, login="owner:ownerpw")
|
||||||
|
|
||||||
|
contact2 = get_file_content("contact2-with-bday.vcf")
|
||||||
|
path2 = path_mapped_2 + "/contact2-with-bday.vcf"
|
||||||
|
path_shared_2 = path_shared_2r + "/contact2-with-bday.vcf"
|
||||||
|
self.put(path2, contact2, login="owner:ownerpw")
|
||||||
|
|
||||||
|
contact3 = get_file_content("contact3-with-bday.vcf")
|
||||||
|
path3 = path_mapped_3 + "/contact3-with-bday.vcf"
|
||||||
|
path_shared_3 = path_shared_3r + "/contact3-with-bday.vcf"
|
||||||
|
self.put(path3, contact3, login="owner:ownerpw")
|
||||||
|
|
||||||
|
# create map
|
||||||
|
logging.info("\n*** create map(bday) user/owner:r -> ok")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped_2
|
||||||
|
json_dict['PathOrToken'] = path_shared_2r
|
||||||
|
json_dict['Conversion'] = "bday"
|
||||||
|
json_dict['Permissions'] = "rP"
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Hidden'] = False
|
||||||
|
json_dict['Properties'] = {"D:displayname": "Test-BDAY2"}
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_summary_template": "{fn} (BDAY2)",
|
||||||
|
"conversion_bday_description_template": "BDAY2={year}-{month}-{day}",
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
answer_dict = json.loads(answer)
|
||||||
|
assert answer_dict['Status'] == "success"
|
||||||
|
|
||||||
|
logging.info("\n*** create map(bday) user/owner:r -> ok")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped_3
|
||||||
|
json_dict['PathOrToken'] = path_shared_3r
|
||||||
|
json_dict['Conversion'] = "bday"
|
||||||
|
json_dict['Permissions'] = "rP"
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Enabled'] = True
|
||||||
|
json_dict['Hidden'] = False
|
||||||
|
json_dict['Properties'] = {"D:displayname": "Test-Birthday3"}
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_summary_template": "{fn} (Birthday3)",
|
||||||
|
"conversion_bday_description_template": "Birthday3={year}-{month}-{day}",
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
answer_dict = json.loads(answer)
|
||||||
|
assert answer_dict['Status'] == "success"
|
||||||
|
|
||||||
|
# enable map by user
|
||||||
|
logging.info("\n*** enable map(bday) by user")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped_2
|
||||||
|
json_dict['PathOrToken'] = path_shared_2r
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
logging.info("\n*** enable map(bday) by user")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped_3
|
||||||
|
json_dict['PathOrToken'] = path_shared_3r
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict)
|
||||||
|
|
||||||
|
# verify content as user
|
||||||
|
logging.info("\n*** GET collection user template 2 -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY2)" in answer
|
||||||
|
|
||||||
|
logging.info("\n*** GET collection user template 3 -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_3, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN-C3 (Birthday3)" in answer
|
||||||
|
|
||||||
|
# update template
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r -> ok")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped_2
|
||||||
|
json_dict['PathOrToken'] = path_shared_2r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_summary_template": "{fn} (BDAY2x)",
|
||||||
|
"conversion_bday_description_template": "BDAY2x={year}-{month}-{day}",
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
answer_dict = json.loads(answer)
|
||||||
|
assert answer_dict['Status'] == "success"
|
||||||
|
|
||||||
|
# verify content as user
|
||||||
|
logging.info("\n*** GET collection user template 2 -> ok")
|
||||||
|
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
|
||||||
|
assert "SUMMARY:Test-FN (BDAY2x)" in answer
|
||||||
|
|
||||||
|
# update template
|
||||||
|
logging.info("\n*** update map(bday) user/owner:r with wrong Action -> problem")
|
||||||
|
json_dict = {}
|
||||||
|
json_dict['User'] = "user"
|
||||||
|
json_dict['PathMapped'] = path_mapped_2
|
||||||
|
json_dict['PathOrToken'] = path_shared_2r
|
||||||
|
json_dict['Actions'] = {"config": {
|
||||||
|
"conversion_bday_summary_template_UNSUPPORTED": "{fn} (BDAY2x)",
|
||||||
|
"conversion_bday_description_template_UNSUPPORTED": "BDAY2x={year}-{month}-{day}",
|
||||||
|
}}
|
||||||
|
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
|
||||||
|
|
||||||
def test_sharing_api_map_vcf_bday_complex(self) -> None:
|
def test_sharing_api_map_vcf_bday_complex(self) -> None:
|
||||||
"""share-by-map with conversion=bday complex tests."""
|
"""share-by-map with conversion=bday complex tests."""
|
||||||
self.configure({"auth": {"type": "htpasswd",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
@@ -5161,7 +5794,7 @@ permissions: RrWw""")
|
|||||||
_, answer = self.get(path_mapped, login="owner:ownerpw")
|
_, answer = self.get(path_mapped, login="owner:ownerpw")
|
||||||
assert "contact1" in answer
|
assert "contact1" in answer
|
||||||
assert "contact2" in answer
|
assert "contact2" in answer
|
||||||
assert "NICKNAME-C3" in answer
|
assert "Family3Test" in answer
|
||||||
|
|
||||||
# create map
|
# create map
|
||||||
logging.info("\n*** create bday owner to itself -> ok")
|
logging.info("\n*** create bday owner to itself -> ok")
|
||||||
@@ -5277,6 +5910,7 @@ permissions: RrWw""")
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"conversion_bday_summary_template": "{fn} (BDAY)",
|
||||||
"permit_create_token": True,
|
"permit_create_token": True,
|
||||||
"permit_properties_overlay": "True",
|
"permit_properties_overlay": "True",
|
||||||
"enforce_properties_overlay": "True",
|
"enforce_properties_overlay": "True",
|
||||||
@@ -5328,7 +5962,7 @@ permissions: RrWw""")
|
|||||||
_, answer = self.get(path_mapped, login="owner:ownerpw")
|
_, answer = self.get(path_mapped, login="owner:ownerpw")
|
||||||
assert "contact1" in answer
|
assert "contact1" in answer
|
||||||
assert "contact2" in answer
|
assert "contact2" in answer
|
||||||
assert "NICKNAME-C3" in answer
|
assert "Family3Test" in answer
|
||||||
|
|
||||||
# create map
|
# create map
|
||||||
logging.info("\n*** create token with bday conversion (default permissions) -> ok")
|
logging.info("\n*** create token with bday conversion (default permissions) -> ok")
|
||||||
@@ -5349,8 +5983,8 @@ permissions: RrWw""")
|
|||||||
logging.info("\n*** GET bday with token")
|
logging.info("\n*** GET bday with token")
|
||||||
_, answer = self.get(path_shared)
|
_, answer = self.get(path_shared)
|
||||||
assert "VCARD" not in answer
|
assert "VCARD" not in answer
|
||||||
assert "Test-FN-C3 (BDAY)" in answer
|
assert "Test-FN-C3 (BDAY)" in answer # contact2
|
||||||
assert "Test-FN (BDAY)" in answer
|
assert "Test-FN (BDAY)" in answer # contact1
|
||||||
|
|
||||||
# verify content as owner
|
# verify content as owner
|
||||||
logging.info("\n*** GET collection owner -> ok")
|
logging.info("\n*** GET collection owner -> ok")
|
||||||
|
|||||||
Reference in New Issue
Block a user