ical.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. def serialize(headers=(), timezones=(), events=(), todos=()):
  35. items = ["BEGIN:VCALENDAR"]
  36. for part in (headers, timezones, todos, events):
  37. if part:
  38. items.append("\n".join(item.text for item in part))
  39. items.append("END:VCALENDAR")
  40. return "\n".join(items)
  41. class Header(object):
  42. """Internal header class."""
  43. def __init__(self, text):
  44. """Initialize header from ``text``."""
  45. self.text = text
  46. class Event(object):
  47. """Internal event class."""
  48. tag = "VEVENT"
  49. def __init__(self, text):
  50. """Initialize event from ``text``."""
  51. self.text = text
  52. @property
  53. def etag(self):
  54. """Etag from event."""
  55. return '"%s"' % hash(self.text)
  56. class Todo(object):
  57. """Internal todo class."""
  58. # This is not a TODO!
  59. # pylint: disable-msg=W0511
  60. tag = "VTODO"
  61. # pylint: enable-msg=W0511
  62. def __init__(self, text):
  63. """Initialize todo from ``text``."""
  64. self.text = text
  65. @property
  66. def etag(self):
  67. """Etag from todo."""
  68. return '"%s"' % hash(self.text)
  69. class Timezone(object):
  70. """Internal timezone class."""
  71. tag = "VTIMEZONE"
  72. def __init__(self, text):
  73. """Initialize timezone from ``text``."""
  74. lines = text.splitlines()
  75. for line in lines:
  76. if line.startswith("TZID:"):
  77. self.name = line.replace("TZID:", "")
  78. break
  79. self.text = text
  80. class Calendar(object):
  81. """Internal calendar class."""
  82. def __init__(self, path):
  83. """Initialize the calendar with ``cal`` and ``user`` parameters."""
  84. # TODO: Use properties from the calendar configuration
  85. self.encoding = "utf-8"
  86. self.owner = path.split("/")[0]
  87. self.path = os.path.join(FOLDER, path.replace("/", os.path.sep))
  88. self.ctag = self.etag
  89. @staticmethod
  90. def _parse(text, obj):
  91. """Find ``obj.tag`` items in ``text`` text.
  92. Return a list of items of type ``obj``.
  93. """
  94. items = []
  95. lines = text.splitlines()
  96. in_item = False
  97. item_lines = []
  98. for line in lines:
  99. if line.startswith("BEGIN:%s" % obj.tag):
  100. in_item = True
  101. item_lines = []
  102. if in_item:
  103. item_lines.append(line)
  104. if line.startswith("END:%s" % obj.tag):
  105. items.append(obj("\n".join(item_lines)))
  106. return items
  107. def append(self, text):
  108. """Append ``text`` to calendar."""
  109. self.ctag = self.etag
  110. timezones = self.timezones
  111. events = self.events
  112. todos = self.todos
  113. for new_timezone in self._parse(text, Timezone):
  114. if new_timezone.name not in [timezone.name
  115. for timezone in timezones]:
  116. timezones.append(new_timezone)
  117. for new_event in self._parse(text, Event):
  118. if new_event.etag not in [event.etag for event in events]:
  119. events.append(new_event)
  120. for new_todo in self._parse(text, Todo):
  121. if new_todo.etag not in [todo.etag for todo in todos]:
  122. todos.append(new_todo)
  123. self.write(timezones=timezones, events=events, todos=todos)
  124. def remove(self, etag):
  125. """Remove object named ``etag`` from the calendar."""
  126. self.ctag = self.etag
  127. todos = [todo for todo in self.todos if todo.etag != etag]
  128. events = [event for event in self.events if event.etag != etag]
  129. self.write(todos=todos, events=events)
  130. def replace(self, etag, text):
  131. """Replace objet named ``etag`` by ``text`` in the calendar."""
  132. self.ctag = self.etag
  133. self.remove(etag)
  134. self.append(text)
  135. def write(self, headers=None, timezones=None, events=None, todos=None):
  136. """Write calendar with given parameters."""
  137. headers = headers or self.headers or (
  138. Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
  139. Header("VERSION:2.0"))
  140. timezones = timezones or self.timezones
  141. events = events or self.events
  142. todos = todos or self.todos
  143. # Create folder if absent
  144. if not os.path.exists(os.path.dirname(self.path)):
  145. os.makedirs(os.path.dirname(self.path))
  146. text = serialize(headers, timezones, events, todos)
  147. return open(self.path, "w").write(text)
  148. @property
  149. def etag(self):
  150. """Etag from calendar."""
  151. return '"%s"' % hash(self.text)
  152. @property
  153. def text(self):
  154. """Calendar as plain text."""
  155. try:
  156. return open(self.path).read()
  157. except IOError:
  158. return ""
  159. @property
  160. def headers(self):
  161. """Find headers items in calendar."""
  162. header_lines = []
  163. lines = self.text.splitlines()
  164. for line in lines:
  165. if line.startswith("PRODID:"):
  166. header_lines.append(Header(line))
  167. for line in lines:
  168. if line.startswith("VERSION:"):
  169. header_lines.append(Header(line))
  170. return header_lines
  171. @property
  172. def events(self):
  173. """Get list of ``Event`` items in calendar."""
  174. return self._parse(self.text, Event)
  175. @property
  176. def todos(self):
  177. """Get list of ``Todo`` items in calendar."""
  178. return self._parse(self.text, Todo)
  179. @property
  180. def timezones(self):
  181. """Get list of ``Timezome`` items in calendar."""
  182. return self._parse(self.text, Timezone)