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
|
||||
|
||||
## 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
|
||||
* 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
|
||||
|
||||
_(>= 3.7.0)_
|
||||
|
||||
Default permissions for create token-based sharing
|
||||
|
||||
Default: `r`
|
||||
@@ -2295,12 +2297,77 @@ Supported: `rwEePp`
|
||||
|
||||
##### default_permissions_create_map
|
||||
|
||||
_(>= 3.7.0)_
|
||||
|
||||
Default permissions for map-based sharing
|
||||
|
||||
Default: `r`
|
||||
|
||||
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
|
||||
|
||||
Radicale has been tested with:
|
||||
|
||||
19
SHARING.md
19
SHARING.md
@@ -43,7 +43,7 @@ Types of supported sharing configuration:
|
||||
* `TimestampCreated`: unixtime of creation
|
||||
* `TimestampUpdated`: unixtime of last update
|
||||
* `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
|
||||
|
||||
@@ -246,6 +246,7 @@ Can be selected by `HTTP_ACCEPT` - default is equal to provided `CONTENT_TYPE`
|
||||
* `Enabled`: owner/user selected by authentication
|
||||
* `Hidden`: owner/user selected by authentication
|
||||
* `Properties`: properties to overlay
|
||||
* `Actions`: specific configuration
|
||||
|
||||
### API Hooks
|
||||
|
||||
@@ -315,7 +316,8 @@ Create a share by mapping a collection of an `Owner` to a token.
|
||||
| Permissions | str | optional (default:rp) |
|
||||
| Enabled | bool | optional (owner/default:False) |
|
||||
| Hidden | bool | optional (owner/default:True) |
|
||||
| Properties | str | optional |
|
||||
| Properties | str(dict) | optional |
|
||||
| Actions | str(dict) | optional |
|
||||
|
||||
* 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) |
|
||||
| Enabled | bool | optional (owner/default:False) |
|
||||
| Hidden | bool | optional (owner/default:True) |
|
||||
| Properties | optional |
|
||||
| Properties | str(dict) | optional |
|
||||
| Actions | str(dict) | optional |
|
||||
|
||||
* 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 |
|
||||
| Enabled | 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
Preconditions:
|
||||
@@ -687,6 +690,8 @@ Preconditions:
|
||||
|
||||
#### Examples using API
|
||||
|
||||
##### Mapping
|
||||
|
||||
* Create as *map*
|
||||
|
||||
```bash
|
||||
@@ -712,6 +717,8 @@ END:VCALENDAR
|
||||
|
||||
Via WebUI an additional (virtual) calendar collection appears
|
||||
|
||||
##### Mapping as token
|
||||
|
||||
* Create as *token*
|
||||
|
||||
```bash
|
||||
@@ -722,7 +729,7 @@ Status='success'
|
||||
PathOrToken='/.token/v1/lqqwqhZYTGi9uSPsixien_8G5jiSK0FfhNFRGG_t8UA0/'
|
||||
```
|
||||
|
||||
* Fetch *map*
|
||||
* Fetch *token*
|
||||
|
||||
```bash
|
||||
## Fetch VCALENDAR auto-created from VADDRESSBOOK
|
||||
|
||||
17
config
17
config
@@ -356,6 +356,23 @@
|
||||
# Supported: rwEePp
|
||||
#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]
|
||||
|
||||
|
||||
@@ -136,9 +136,9 @@ class ApplicationPartGet(ApplicationBase):
|
||||
if share and share['Conversion'] == "bday":
|
||||
if isinstance(item, storage.BaseCollection):
|
||||
# convert VCF to ICS
|
||||
answer = item.serialize(vcf_to_ics=True)
|
||||
answer = item.serialize(vcf_to_ics=True, ShareActions=share['Actions'])
|
||||
else:
|
||||
item_converted = item.convert_vcf_to_ics()
|
||||
item_converted = item.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||
if item_converted is not None:
|
||||
answer = item_converted.serialize()
|
||||
else:
|
||||
|
||||
@@ -230,7 +230,7 @@ def xml_propfind_response(
|
||||
element.text = item.etag
|
||||
else:
|
||||
if share_bday_automap:
|
||||
item_converted = item.convert_vcf_to_ics()
|
||||
item_converted = item.convert_vcf_to_ics(ShareActions=share['Actions'])
|
||||
if item_converted:
|
||||
element.text = item_converted.etag
|
||||
else:
|
||||
@@ -382,14 +382,14 @@ def xml_propfind_response(
|
||||
logger.trace("PROPFIND/xml_propfind_response/getcontentlength: start bday automap handling for collection")
|
||||
length = 0
|
||||
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:
|
||||
continue
|
||||
length += len(item_ics.vobject_item.serialize().encode(encoding))
|
||||
element.text = str(length)
|
||||
else:
|
||||
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:
|
||||
element.text = str(len(item_converted.serialize()))
|
||||
else:
|
||||
@@ -457,7 +457,7 @@ def xml_propfind_response(
|
||||
logger.trace("PROPFIND/xml_propfind_response/getcontentcount: start bday automap handling")
|
||||
items = []
|
||||
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:
|
||||
continue
|
||||
items.append(item_ics.vobject_item)
|
||||
@@ -626,7 +626,7 @@ class ApplicationPartPropfind(ApplicationBase):
|
||||
for item, permission, raw_permissions in item_list:
|
||||
if self._sharing._enabled and share:
|
||||
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:
|
||||
# only dedicated item requested
|
||||
return httputils.NOT_FOUND
|
||||
|
||||
@@ -267,7 +267,7 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
|
||||
# autoconvert
|
||||
retrieved_items_vcf_to_ics = []
|
||||
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:
|
||||
continue
|
||||
else:
|
||||
|
||||
@@ -593,7 +593,28 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
||||
("default_permissions_create_map", {
|
||||
"value": "r",
|
||||
"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([
|
||||
("type", {
|
||||
"value": "none",
|
||||
|
||||
@@ -38,7 +38,7 @@ from typing import (Any, Callable, List, MutableMapping, Optional, Sequence,
|
||||
import vobject
|
||||
|
||||
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.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") + ")"
|
||||
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]:
|
||||
"""Wrapper for vobject.readComponents"""
|
||||
@@ -362,6 +364,65 @@ def verify(file: str, encoding: str):
|
||||
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 for address book and calendar entries."""
|
||||
|
||||
@@ -504,7 +565,8 @@ class Item:
|
||||
self.component_name
|
||||
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 (vobject): %r", self.vobject_item)
|
||||
if self.vobject_item.name != "VCARD":
|
||||
@@ -529,23 +591,54 @@ class Item:
|
||||
else:
|
||||
pass
|
||||
|
||||
placeholder_mapping: dict = {}
|
||||
|
||||
bdayS = match[1] + match[2] + match[3]
|
||||
bdaySdesc = match[1] + "-" + match[2] + "-" + match[3]
|
||||
bdayY = int(match[1])
|
||||
bdayM = int(match[2])
|
||||
bdayD = int(match[3])
|
||||
|
||||
placeholder_mapping['{year}'] = match[1]
|
||||
placeholder_mapping['{month}'] = match[2]
|
||||
placeholder_mapping['{day}'] = match[3]
|
||||
|
||||
# create ICS
|
||||
if hasattr(self.vobject_item, "fn"):
|
||||
name = self.vobject_item.fn.value
|
||||
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"):
|
||||
name = self.vobject_item.nickname.value
|
||||
else:
|
||||
logger.trace("item/convert_vcf_to_ics: has bday but neither FN or N or NICKNAME (skip): %r", self.href)
|
||||
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
|
||||
item_ics = vobject.newFromBehavior('vcalendar')
|
||||
|
||||
@@ -555,40 +648,134 @@ class Item:
|
||||
else:
|
||||
item_ics.add('prodid').value = PRODID_CONVERTED
|
||||
|
||||
# create EVENT
|
||||
item_ics.add('vevent')
|
||||
# prepare SUMMARY
|
||||
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
|
||||
dtstart = datetime.date(bdayY, bdayM, bdayD)
|
||||
item_ics.vevent.add('dtstart').value = dtstart
|
||||
# prepare DESCRIPTION
|
||||
if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_description_template' in ShareActions['config']:
|
||||
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
|
||||
dtend = dtstart + datetime.timedelta(days=1)
|
||||
item_ics.vevent.add('dtend').value = dtend
|
||||
# create CATEGORIES
|
||||
if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_categories' in ShareActions['config']:
|
||||
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"):
|
||||
pattern = re.compile('^(.*)-[0-9a-fA-F]{12}(.*)$')
|
||||
match = pattern.match(self.vobject_item.uid.value)
|
||||
if match:
|
||||
# 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:
|
||||
item_ics.vevent.add('uid').value = self.vobject_item.uid.value + UID_SUFFIX
|
||||
uid = self.vobject_item.uid.value + UID_SUFFIX
|
||||
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
|
||||
item_ics.vevent.add('summary').value = name + " (BDAY)"
|
||||
age = 0
|
||||
while age <= age_max:
|
||||
# create EVENT
|
||||
vevent = item_ics.add('vevent')
|
||||
|
||||
# set RRULE
|
||||
item_ics.vevent.add('rrule').value = "FREQ=YEARLY"
|
||||
# set DTSTART
|
||||
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
|
||||
item_ics.vevent.add('transp').value = "TRANSPARENT"
|
||||
# calculate and set DTEND
|
||||
dtend = dtstart + datetime.timedelta(days=1)
|
||||
vevent.add('dtend').value = dtend
|
||||
|
||||
# add description
|
||||
item_ics.vevent.add('description').value = "BDAY=" + bdaySdesc
|
||||
# set UID
|
||||
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
|
||||
if href is not None:
|
||||
|
||||
@@ -24,11 +24,11 @@ import uuid
|
||||
from csv import DictWriter
|
||||
from datetime import datetime
|
||||
from http import client
|
||||
from typing import Sequence, Union
|
||||
from typing import Any, Sequence, Union
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from radicale import (config, httputils, pathutils, rights, storage, types,
|
||||
utils)
|
||||
from radicale import (config, httputils, item, pathutils, rights, storage,
|
||||
types, utils)
|
||||
from radicale.log import logger
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
|
||||
@@ -166,6 +237,11 @@ class BaseSharing:
|
||||
self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map")
|
||||
self.permit_properties_overlay = configuration.get("sharing", "permit_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_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.permit_properties_overlay: %s", self.permit_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
|
||||
self.sharing_db_type = configuration.get("sharing", "type")
|
||||
@@ -403,6 +484,73 @@ class BaseSharing:
|
||||
else:
|
||||
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
|
||||
|
||||
# adjust a share
|
||||
@@ -789,7 +937,31 @@ class BaseSharing:
|
||||
return httputils.bad_request("Conversion not supported: %r" % Conversion)
|
||||
|
||||
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:
|
||||
Enabled = request_data['Enabled']
|
||||
@@ -1112,7 +1284,8 @@ class BaseSharing:
|
||||
OwnerOrUser=user,
|
||||
User=User,
|
||||
Timestamp=Timestamp,
|
||||
Properties=Properties)
|
||||
Properties=Properties,
|
||||
Actions=Actions)
|
||||
else:
|
||||
result = self.database_update_sharing(
|
||||
ShareType=ShareType,
|
||||
@@ -1124,7 +1297,8 @@ class BaseSharing:
|
||||
OwnerOrUser=user,
|
||||
User=User,
|
||||
Timestamp=Timestamp,
|
||||
Properties=Properties)
|
||||
Properties=Properties,
|
||||
Actions=Actions)
|
||||
|
||||
elif user == share['User']:
|
||||
# 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 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 ': -> ":(whitespace)
|
||||
field = field.replace("', '", '", "') # replace for JSON delimiter ', ' -> ", "
|
||||
logger.trace("json prep quote replacer match (after) : %s", field)
|
||||
try:
|
||||
|
||||
@@ -223,7 +223,7 @@ class BaseCollection:
|
||||
"""Get the HTTP-datetime of when the collection was modified."""
|
||||
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."""
|
||||
if self.tag == "VCALENDAR":
|
||||
in_vcalendar = False
|
||||
@@ -288,7 +288,7 @@ class BaseCollection:
|
||||
items = []
|
||||
for item in self.get_all():
|
||||
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:
|
||||
continue
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,7 @@ BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:+//IDN bitfire.at//DAVx5/3.3.5-ose ez-vcard/0.11.0
|
||||
UID:contact2-with-bday
|
||||
N:Test;N;;;
|
||||
N:FamilyTest;GivenTest;AdditionalsTest;;
|
||||
FN:Test-FN
|
||||
NICKNAME:Test-NICKNAME
|
||||
BDAY:1970-01-01
|
||||
|
||||
@@ -2,8 +2,7 @@ BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:+//Manual/0.0.1
|
||||
UID:05cf4901-e581-44ee-bb0a-1e1a875da44a
|
||||
N:Test;N;C3;;
|
||||
N:Family3Test;Given3Test;;;
|
||||
FN:Test-FN-C3
|
||||
NICKNAME:Test-NICKNAME-C3
|
||||
BDAY:1990-01-01
|
||||
END:VCARD
|
||||
|
||||
@@ -301,7 +301,7 @@ class TestBaseAuthRequests(BaseTest):
|
||||
delay = .3
|
||||
delay_min = delay * 0.9 # 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
|
||||
|
||||
time_begin = datetime.datetime.now()
|
||||
|
||||
@@ -4830,6 +4830,7 @@ permissions: RrWw""")
|
||||
assert "DTEND;VALUE=DATE:19700102" in answer
|
||||
assert "TRANSP:TRANSPARENT" in answer
|
||||
assert "DESCRIPTION:BDAY=1970-01-01" in answer
|
||||
assert "CATEGORIES:Birthday" in answer
|
||||
# content type must be adjusted
|
||||
assert 'Content-Type' in headers
|
||||
assert 'text/calendar' in headers['Content-Type']
|
||||
@@ -4879,6 +4880,9 @@ permissions: RrWw""")
|
||||
assert "DTEND;VALUE=DATE:19700102" in answer
|
||||
assert "TRANSP:TRANSPARENT" 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
|
||||
assert 'Content-Type' in headers
|
||||
assert 'text/calendar' in headers['Content-Type']
|
||||
@@ -4931,6 +4935,635 @@ permissions: RrWw""")
|
||||
status, prop = response["D:getcontenttype"]
|
||||
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:
|
||||
"""share-by-map with conversion=bday complex tests."""
|
||||
self.configure({"auth": {"type": "htpasswd",
|
||||
@@ -5161,7 +5794,7 @@ permissions: RrWw""")
|
||||
_, answer = self.get(path_mapped, login="owner:ownerpw")
|
||||
assert "contact1" in answer
|
||||
assert "contact2" in answer
|
||||
assert "NICKNAME-C3" in answer
|
||||
assert "Family3Test" in answer
|
||||
|
||||
# create map
|
||||
logging.info("\n*** create bday owner to itself -> ok")
|
||||
@@ -5277,6 +5910,7 @@ permissions: RrWw""")
|
||||
"htpasswd_encryption": "plain"},
|
||||
"sharing": {
|
||||
"type": "csv",
|
||||
"conversion_bday_summary_template": "{fn} (BDAY)",
|
||||
"permit_create_token": True,
|
||||
"permit_properties_overlay": "True",
|
||||
"enforce_properties_overlay": "True",
|
||||
@@ -5328,7 +5962,7 @@ permissions: RrWw""")
|
||||
_, answer = self.get(path_mapped, login="owner:ownerpw")
|
||||
assert "contact1" in answer
|
||||
assert "contact2" in answer
|
||||
assert "NICKNAME-C3" in answer
|
||||
assert "Family3Test" in answer
|
||||
|
||||
# create map
|
||||
logging.info("\n*** create token with bday conversion (default permissions) -> ok")
|
||||
@@ -5349,8 +5983,8 @@ permissions: RrWw""")
|
||||
logging.info("\n*** GET bday with token")
|
||||
_, answer = self.get(path_shared)
|
||||
assert "VCARD" not in answer
|
||||
assert "Test-FN-C3 (BDAY)" in answer
|
||||
assert "Test-FN (BDAY)" in answer
|
||||
assert "Test-FN-C3 (BDAY)" in answer # contact2
|
||||
assert "Test-FN (BDAY)" in answer # contact1
|
||||
|
||||
# verify content as owner
|
||||
logging.info("\n*** GET collection owner -> ok")
|
||||
|
||||
Reference in New Issue
Block a user