123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- import caldav
- import os
-
- import re
- import transaction
- from caldav.lib.error import PutError
-
- from icalendar import Event as iCalendarEvent
- from sqlalchemy.orm.exc import NoResultFound
- from tg import tmpl_context
- from tg.i18n import ugettext as _
-
- from tracim.lib.content import ContentApi
- from tracim.lib.exceptions import UnknownCalendarType
- from tracim.lib.exceptions import NotFound
- from tracim.lib.user import UserApi
- from tracim.lib.workspace import UnsafeWorkspaceApi
- from tracim.lib.workspace import WorkspaceApi
- from tracim.model import User
- from tracim.model import DBSession
- from tracim.model import new_revision
- from tracim.model.data import ActionDescription
- from tracim.model.data import Content
- from tracim.model.data import ContentType
- from tracim.model.organisational import Calendar
- from tracim.model.organisational import UserCalendar
- from tracim.model.organisational import WorkspaceCalendar
-
- CALENDAR_USER_PATH_RE = 'user\/([0-9]+).ics'
- CALENDAR_WORKSPACE_PATH_RE = 'workspace\/([0-9]+).ics'
-
- CALENDAR_TYPE_USER = UserCalendar
- CALENDAR_TYPE_WORKSPACE = WorkspaceCalendar
-
- CALENDAR_USER_URL_TEMPLATE = 'user/{id}.ics/'
- CALENDAR_WORKSPACE_URL_TEMPLATE = 'workspace/{id}.ics/'
-
- CALENDAR_USER_BASE_URL = '/user/'
- CALENDAR_WORKSPACE_BASE_URL = '/workspace/'
-
-
- class CalendarManager(object):
- @classmethod
- def get_personal_calendar_description(cls) -> str:
- return _('My personal calendar')
-
- @classmethod
- def get_base_url(cls, low_level: bool=False) -> str:
- """
- :param low_level: If True, use local ip address with radicale port.
- :return: Radical address base url.
- """
- from tracim.config.app_cfg import CFG
- cfg = CFG.get_instance()
-
- if not low_level:
- return cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE
-
- return 'http://127.0.0.1:{0}'.format(cfg.RADICALE_SERVER_PORT)
-
- @classmethod
- def get_user_base_url(cls):
- from tracim.config.app_cfg import CFG
- cfg = CFG.get_instance()
- return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE, 'user/')
-
- @classmethod
- def get_workspace_base_url(cls):
- from tracim.config.app_cfg import CFG
- cfg = CFG.get_instance()
- return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE, 'workspace/')
-
- @classmethod
- def get_user_calendar_url(
- cls,
- user_id: int,
- low_level: bool=False,
- ):
- user_path = CALENDAR_USER_URL_TEMPLATE.format(id=str(user_id))
- return os.path.join(
- cls.get_base_url(low_level=low_level),
- user_path,
- )
-
- @classmethod
- def get_workspace_calendar_url(
- cls,
- workspace_id: int,
- low_level: bool=False,
- ):
- workspace_path = CALENDAR_WORKSPACE_URL_TEMPLATE.format(
- id=str(workspace_id)
- )
- return os.path.join(
- cls.get_base_url(low_level=low_level),
- workspace_path,
- )
-
- def __init__(self, user: User):
- self._user = user
-
- def get_type_for_path(self, path: str) -> str:
- """
- Return calendar type for given path. Raise
- tracim.lib.exceptions.UnknownCalendarType if unknown type.
- :param path: path representation like user/42--foo.ics
- :return: Type of calendar, can be one of CALENDAR_TYPE_USER,
- CALENDAR_TYPE_WORKSPACE
- """
- if re.match(CALENDAR_USER_PATH_RE, path):
- return CALENDAR_TYPE_USER
-
- if re.match(CALENDAR_WORKSPACE_PATH_RE, path):
- return CALENDAR_TYPE_WORKSPACE
-
- raise UnknownCalendarType(
- 'No match for calendar path "{0}"'.format(path)
- )
-
- def get_id_for_path(self, path: str, type: str) -> int:
- """
- Return related calendar id for given path. Raise
- tracim.lib.exceptions.UnknownCalendarType if unknown type.
- :param path: path representation like user/42--foo.ics
- :param type: Type of calendar, can be one of CALENDAR_TYPE_USER,
- CALENDAR_TYPE_WORKSPACE
- :return: ID of related calendar object. For UserCalendar it will be
- user id, for WorkspaceCalendar it will be Workspace id.
- """
- if type == CALENDAR_TYPE_USER:
- return re.search(CALENDAR_USER_PATH_RE, path).group(1)
- elif type == CALENDAR_TYPE_WORKSPACE:
- return re.search(CALENDAR_WORKSPACE_PATH_RE, path).group(1)
- raise UnknownCalendarType('Type "{0}" is not implemented'.format(type))
-
- def find_calendar_with_path(self, path: str) -> Calendar:
- """
- Return calendar for given path. Raise tracim.lib.exceptions.NotFound if
- calendar cannot be found.
- :param path: path representation like user/42--foo.ics
- :return: Calendar corresponding to path
- """
- try:
- type = self.get_type_for_path(path)
- id = self.get_id_for_path(path, type)
- except UnknownCalendarType as exc:
- raise NotFound(str(exc))
-
- try:
- return self.get_calendar(type, id, path)
- except NoResultFound as exc:
- raise NotFound(str(exc))
-
- def get_calendar(self, type: str, id: str, path: str) -> Calendar:
- """
- Return tracim.model.organisational.Calendar instance for given
- parameters.
- :param type: Type of calendar, can be one of CALENDAR_TYPE_USER,
- CALENDAR_TYPE_WORKSPACE
- :param id: related calendar object id
- :param path: path representation like user/42--foo.ics
- :return: a calendar.
- """
- if type == CALENDAR_TYPE_USER:
- user = UserApi(self._user).get_one_by_id(id)
- return UserCalendar(user, path=path)
-
- if type == CALENDAR_TYPE_WORKSPACE:
- workspace = UnsafeWorkspaceApi(self._user).get_one(id)
- return WorkspaceCalendar(workspace, path=path)
-
- raise UnknownCalendarType('Type "{0}" is not implemented'.format(type))
-
- def add_event(
- self,
- calendar: Calendar,
- event: iCalendarEvent,
- event_name: str,
- owner: User,
- ) -> Content:
- """
- Create Content event type.
- :param calendar: Event calendar owner
- :param event: ICS event
- :param event_name: Event name (ID) like
- 20160602T083511Z-18100-1001-1-71_Bastien-20160602T083516Z.ics
- :param owner: Event Owner
- :return: Created Content
- """
- workspace = None
- if isinstance(calendar, WorkspaceCalendar):
- workspace = calendar.related_object
- elif isinstance(calendar, UserCalendar):
- pass
- else:
- raise UnknownCalendarType('Type "{0}" is not implemented'
- .format(type(calendar)))
-
- content = ContentApi(owner).create(
- content_type=ContentType.Event,
- workspace=workspace,
- do_save=False
- )
- self.populate_content_with_event(
- content,
- event,
- event_name
- )
- content.revision_type = ActionDescription.CREATION
- DBSession.add(content)
- DBSession.flush()
- transaction.commit()
-
- return content
-
- def update_event(
- self,
- calendar: Calendar,
- event: iCalendarEvent,
- event_name: str,
- current_user: User,
- ) -> Content:
- """
- Update Content Event
- :param calendar: Event calendar owner
- :param event: ICS event
- :param event_name: Event name (ID) like
- 20160602T083511Z-18100-1001-1-71_Bastien-20160602T083516Z.ics
- :param current_user: Current modification asking user
- :return: Updated Content
- """
- workspace = None
- if isinstance(calendar, WorkspaceCalendar):
- workspace = calendar.related_object
- elif isinstance(calendar, UserCalendar):
- pass
- else:
- raise UnknownCalendarType('Type "{0}" is not implemented'
- .format(type(calendar)))
-
- content_api = ContentApi(
- current_user,
- force_show_all_types=True,
- disable_user_workspaces_filter=True
- )
- content = content_api.find_one_by_unique_property(
- property_name='name',
- property_value=event_name,
- workspace=workspace
- )
-
- with new_revision(content):
- self.populate_content_with_event(
- content,
- event,
- event_name
- )
- content.revision_type = ActionDescription.EDITION
-
- DBSession.flush()
- transaction.commit()
-
- return content
-
- def delete_event_with_name(self, event_name: str, current_user: User)\
- -> Content:
- """
- Delete Content Event
- :param event_name: Event name (ID) like
- 20160602T083511Z-18100-1001-1-71_Bastien-20160602T083516Z.ics
- :param current_user: Current deletion asking user
- :return: Deleted Content
- """
- content_api = ContentApi(current_user, force_show_all_types=True)
- content = content_api.find_one_by_unique_property(
- property_name='name',
- property_value=event_name,
- workspace=None
- )
-
- with new_revision(content):
- content_api.delete(content)
-
- DBSession.flush()
- transaction.commit()
-
- return content
-
- def populate_content_with_event(
- self,
- content: Content,
- event: iCalendarEvent,
- event_name: str,
- ) -> None:
- """
- Populate Content content instance from iCalendarEvent event attributes.
- :param content: content to populate
- :param event: event with data to insert in content
- :param event_name: Event name (ID) like
- 20160602T083511Z-18100-1001-1-71_Bastien-20160602T083516Z.ics
- :return: given content
- """
- content.label = event.get('summary')
- content.description = event.get('description')
- content.properties = {
- 'name': event_name,
- 'location': event.get('location'),
- 'raw': event.to_ical().decode("utf-8"),
- 'start': event.get('dtend').dt.strftime('%Y-%m-%d %H:%M:%S%z'),
- 'end': event.get('dtstart').dt.strftime('%Y-%m-%d %H:%M:%S%z'),
- }
-
- @classmethod
- def get_workspace_readable_calendars_urls_for_user(cls, user: User)\
- -> [str]:
- calendar_urls = []
- for workspace in cls.get_workspace_readable_calendars_for_user(user):
- calendar_urls.append(cls.get_workspace_calendar_url(
- workspace_id=workspace.workspace_id,
- ))
-
- return calendar_urls
-
- @classmethod
- def get_workspace_readable_calendars_for_user(cls, user: User)\
- -> ['Workspace']:
- workspaces = []
- workspace_api = WorkspaceApi(user)
-
- for workspace in workspace_api.get_all():
- if workspace.calendar_enabled:
- workspaces.append(workspace)
-
- return workspaces
-
- def is_discovery_path(self, path: str) -> bool:
- """
- If collection url in one of them, Caldav client is tring to discover
- collections.
- :param path: collection path
- :return: True if given collection path is an discover path
- """
- return path in ('user', 'workspace')
-
- def create_then_remove_fake_event(
- self,
- calendar_class,
- related_object_id,
- ) -> None:
- radicale_base_url = self.get_base_url(low_level=True)
- client = caldav.DAVClient(
- radicale_base_url,
- username=self._user.email,
- password=self._user.auth_token,
- )
- if calendar_class == WorkspaceCalendar:
- calendar_url = self.get_workspace_calendar_url(
- related_object_id,
- low_level=True,
- )
- elif calendar_class == UserCalendar:
- calendar_url = self.get_user_calendar_url(
- related_object_id,
- low_level=True,
- )
- else:
- raise Exception('Unknown calendar type {0}'.format(calendar_class))
-
- user_calendar = caldav.Calendar(
- parent=client,
- client=client,
- url=calendar_url
- )
-
- event_ics = """BEGIN:VCALENDAR
- VERSION:2.0
- PRODID:-//Example Corp.//CalDAV Client//EN
- BEGIN:VEVENT
- UID:{uid}
- DTSTAMP:20100510T182145Z
- DTSTART:20100512T170000Z
- DTEND:20100512T180000Z
- SUMMARY:This is an event
- LOCATION:Here
- END:VEVENT
- END:VCALENDAR
- """.format(uid='{0}FAKEEVENT'.format(related_object_id))
- try:
- event = user_calendar.add_event(event_ics)
- event.delete()
- except PutError:
- pass # TODO BS 20161128: Radicale is down. Record this event ?
-
- def get_enabled_calendar_file_path(
- self,
- calendar_class,
- related_object_id,
- ) -> str:
- from tracim.config.app_cfg import CFG
- cfg = CFG.get_instance()
-
- calendar_file_name = '{}.ics'.format(related_object_id)
-
- if calendar_class == WorkspaceCalendar:
- calendar_type_folder = 'workspace'
- elif calendar_class == UserCalendar:
- calendar_type_folder = 'user'
- else:
- raise NotImplementedError()
-
- current_calendar_file_path = os.path.join(
- cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER,
- calendar_type_folder,
- calendar_file_name,
- )
-
- return current_calendar_file_path
-
- def get_deleted_calendar_file_path(
- self,
- calendar_class,
- related_object_id,
- ) -> str:
- from tracim.config.app_cfg import CFG
- cfg = CFG.get_instance()
-
- calendar_file_name = '{}.ics'.format(related_object_id)
-
- if calendar_class == WorkspaceCalendar:
- calendar_type_folder = 'workspace'
- elif calendar_class == UserCalendar:
- calendar_type_folder = 'user'
- else:
- raise NotImplementedError()
-
- deleted_calendar_file_path = os.path.join(
- cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER,
- calendar_type_folder,
- 'deleted',
- calendar_file_name,
- )
-
- return deleted_calendar_file_path
-
- def disable_calendar_file(
- self,
- calendar_class,
- related_object_id: int,
- raise_: bool=False,
- ) -> None:
- enabled_calendar_file_path = self.get_enabled_calendar_file_path(
- calendar_class,
- related_object_id,
- )
- deleted_calendar_file_path = self.get_deleted_calendar_file_path(
- calendar_class,
- related_object_id,
- )
-
- deleted_folder = os.path.dirname(deleted_calendar_file_path)
- if not os.path.exists(deleted_folder):
- os.makedirs(deleted_folder)
-
- try:
- os.rename(enabled_calendar_file_path, deleted_calendar_file_path)
- except FileNotFoundError:
- if raise_:
- raise
-
- def enable_calendar_file(
- self,
- calendar_class,
- related_object_id: int,
- raise_: bool=False,
- ) -> None:
- enabled_calendar_file_path = self.get_enabled_calendar_file_path(
- calendar_class,
- related_object_id,
- )
- deleted_calendar_file_path = self.get_deleted_calendar_file_path(
- calendar_class,
- related_object_id,
- )
-
- try:
- os.rename(deleted_calendar_file_path, enabled_calendar_file_path)
- except FileNotFoundError:
- if raise_:
- raise
|