calendar.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Radicale Server - Calendar Server
  4. # Copyright © 2008-2010 Guillaume Ayoub
  5. # Copyright © 2008 Nicolas Kandel
  6. # Copyright © 2008 Pascal Halter
  7. #
  8. # This library is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  20. """
  21. Radicale calendar classes.
  22. Define the main classes of a calendar as seen from the server.
  23. """
  24. import os
  25. import codecs
  26. from radicale import config
  27. FOLDER = os.path.expanduser(config.get("storage", "folder"))
  28. # This function overrides the builtin ``open`` function for this module
  29. # pylint: disable-msg=W0622
  30. def open(path, mode="r"):
  31. """Open file at ``path`` with ``mode``, automagically managing encoding."""
  32. return codecs.open(path, mode, config.get("encoding", "stock"))
  33. # pylint: enable-msg=W0622
  34. class Header(object):
  35. """Internal header class."""
  36. def __init__(self, text):
  37. """Initialize header from ``text``."""
  38. self.text = text
  39. class Event(object):
  40. """Internal event class."""
  41. tag = "VEVENT"
  42. def __init__(self, text):
  43. """Initialize event from ``text``."""
  44. self.text = text
  45. @property
  46. def etag(self):
  47. """Etag from event."""
  48. return '"%s"' % hash(self.text)
  49. class Todo(object):
  50. """Internal todo class."""
  51. # This is not a TODO!
  52. # pylint: disable-msg=W0511
  53. tag = "VTODO"
  54. # pylint: enable-msg=W0511
  55. def __init__(self, text):
  56. """Initialize todo from ``text``."""
  57. self.text = text
  58. @property
  59. def etag(self):
  60. """Etag from todo."""
  61. return '"%s"' % hash(self.text)
  62. class Timezone(object):
  63. """Internal timezone class."""
  64. tag = "VTIMEZONE"
  65. def __init__(self, text):
  66. """Initialize timezone from ``text``."""
  67. lines = text.splitlines()
  68. for line in lines:
  69. if line.startswith("TZID:"):
  70. self.name = line.replace("TZID:", "")
  71. break
  72. self.text = text
  73. class Calendar(object):
  74. """Internal calendar class."""
  75. def __init__(self, path):
  76. """Initialize the calendar with ``cal`` and ``user`` parameters."""
  77. # TODO: Use properties from the calendar configuration
  78. self.encoding = "utf-8"
  79. self.owner = path.split("/")[0]
  80. self.path = os.path.join(FOLDER, path.replace("/", os.path.sep))
  81. self.ctag = self.etag
  82. @staticmethod
  83. def _parse(text, obj):
  84. """Find ``obj.tag`` items in ``text`` text.
  85. Return a list of items of type ``obj``.
  86. """
  87. items = []
  88. lines = text.splitlines()
  89. in_item = False
  90. item_lines = []
  91. for line in lines:
  92. if line.startswith("BEGIN:%s" % obj.tag):
  93. in_item = True
  94. item_lines = []
  95. if in_item:
  96. item_lines.append(line)
  97. if line.startswith("END:%s" % obj.tag):
  98. items.append(obj("\n".join(item_lines)))
  99. return items
  100. def append(self, text):
  101. """Append ``text`` to calendar."""
  102. self.ctag = self.etag
  103. timezones = self.timezones
  104. events = self.events
  105. todos = self.todos
  106. for new_timezone in self._parse(text, Timezone):
  107. if new_timezone.name not in [timezone.name
  108. for timezone in timezones]:
  109. timezones.append(new_timezone)
  110. for new_event in self._parse(text, Event):
  111. if new_event.etag not in [event.etag for event in events]:
  112. events.append(new_event)
  113. for new_todo in self._parse(text, Todo):
  114. if new_todo.etag not in [todo.etag for todo in todos]:
  115. todos.append(new_todo)
  116. self.write(timezones=timezones, events=events, todos=todos)
  117. def remove(self, etag):
  118. """Remove object named ``etag`` from the calendar."""
  119. self.ctag = self.etag
  120. todos = [todo for todo in self.todos if todo.etag != etag]
  121. events = [event for event in self.events if event.etag != etag]
  122. self.write(todos=todos, events=events)
  123. def replace(self, etag, text):
  124. """Replace objet named ``etag`` by ``text`` in the calendar."""
  125. self.ctag = self.etag
  126. self.remove(etag)
  127. self.append(text)
  128. def write(self, headers=None, timezones=None, events=None, todos=None):
  129. """Write calendar with given parameters."""
  130. headers = headers or self.headers or (
  131. Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
  132. Header("VERSION:2.0"))
  133. timezones = timezones or self.timezones
  134. events = events or self.events
  135. todos = todos or self.todos
  136. # Create folder if absent
  137. if not os.path.exists(os.path.dirname(self.path)):
  138. os.makedirs(os.path.dirname(self.path))
  139. text = "\n".join((
  140. "BEGIN:VCALENDAR",
  141. "\n".join([header.text for header in headers]),
  142. "\n".join([timezone.text for timezone in timezones]),
  143. "\n".join([todo.text for todo in todos]),
  144. "\n".join([event.text for event in events]),
  145. "END:VCALENDAR"))
  146. return open(self.path, "w").write(text)
  147. @property
  148. def etag(self):
  149. """Etag from calendar."""
  150. return '"%s"' % hash(self.text)
  151. @property
  152. def text(self):
  153. """Calendar as plain text."""
  154. try:
  155. return open(self.path).read()
  156. except IOError:
  157. return ""
  158. @property
  159. def headers(self):
  160. """Find headers items in calendar."""
  161. header_lines = []
  162. lines = self.text.splitlines()
  163. for line in lines:
  164. if line.startswith("PRODID:"):
  165. header_lines.append(Header(line))
  166. for line in lines:
  167. if line.startswith("VERSION:"):
  168. header_lines.append(Header(line))
  169. return header_lines
  170. @property
  171. def events(self):
  172. """Get list of ``Event`` items in calendar."""
  173. return self._parse(self.text, Event)
  174. @property
  175. def todos(self):
  176. """Get list of ``Todo`` items in calendar."""
  177. return self._parse(self.text, Todo)
  178. @property
  179. def timezones(self):
  180. """Get list of ``Timezome`` items in calendar."""
  181. return self._parse(self.text, Timezone)