At the Forge - 动态生成的日历

作者:Reuven M. Lerner

在上个月的专栏中,我们研究了 Sunbird,这是一个来自 Mozilla 基金会的独立应用程序,用于跟踪日历。正如我们所见,Sunbird 能够处理 iCalendar 格式的日历。这些日历可能位于本地文件系统上,也可能通过 HTTP 从远程服务器检索。我们还了解了 Sunbird 如何轻松使用远程服务器提供的日历。我们只需在对话框中输入 URL,等待 Sunbird 检索 iCalendar 文件后,新事件就会添加到我们的日历显示中。

互联网上已经存在各种 iCalendar 格式的远程日历,您可以毫不费力地找到并订阅它们。但是,这样做仅在您想要订阅已存在或公开可用的日历时才有用。如果您的组织想要将 iCalendar 标准化以交换事件信息怎么办?您如何创建和分发 iCalendar 文件,以便其他人可以跟踪他们必须参加的事件?

本月,我们将着眼于 iCalendar 文件的服务器端,并创建旨在由组织内的日历应用程序(如 Sunbird)检索的日历。

iCalendar 文件

如果两台计算机要交换日历,我们显然需要一个标准来定义这些日历的格式。它们交换的协议未定义,尽管标准和日常使用都表明 HTTP 是此类事务的首选。RFC 2445 中定义的日历交换格式反映了它的年代。虽然新的日历格式无疑会被定义为使用 XML,但此 RFC 的日期为 1998 年 11 月,它使用一组名称-值对,并在层次结构中对元素进行一些原始嵌套。例如,这是我们上个月首次查看 Sunbird 时检查的 iCalendar 文件

BEGIN:VCALENDAR
VERSION
 :2.0
PRODID
 :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN
BEGIN:VEVENT
UID
 :05e55cc2-1dd2-11b2-8818-f578cbb4b77d
SUMMARY
 :LJ deadline
STATUS
 :TENTATIVE
CLASS
 :PRIVATE
X-MOZILLA-ALARM-DEFAULT-LENGTH
 :0
DTSTART
 :20050211T140000
DTEND
 :20050211T150000
DTSTAMP
 :20050209T132231Z
END:VEVENT
END:VCALENDAR

正如您所看到的,该文件以 BEGIN:VCALENDAR 和 END:VCALENDAR 标签开始和结束。文件顶部有一些日历范围的数据,VERSION 和 PRODID,但随后定义了第一个也是唯一的事件,用 BEGIN:VEVENT 和 END:VEVENT 条目括起来。您可以想象一个文件可能比这单个条目有更多条目。

iCalendar 使事件可以按固定的时间间隔重复发生。因此,您可以有一个 VEVENT 条目提醒您每周一下午的会议,或者提醒您每周二和周五早上倒垃圾。每个事件也有开始和结束时间 DTSTART 和 DTEND,允许不同的时长。

虽然从上面的示例中并不明显,但 iCalendar 还允许我们对重复事件进行例外处理。因此,如果您的每周一下午会议在节假日期间不会举行,您可以插入一个 EXDATE 条目。然后,显示您日历的应用程序将忽略该日期的重复事件。

发布 iCalendar 文件

假设我们的系统上已经有了一个 iCalendar 文件,那么在 Web 上使其可用非常容易。列表 1 包含一个简单的 CGI 程序,我用 Python 编写的;它在特定目录中查找 iCalendar 文件,并将该文件的内容返回给请求的日历应用程序。

列表 1. static-calendar.py,一个简单的 Python CGI 程序,用于打开 iCalendar 文件并通过 HTTP 发送它。

#!/usr/bin/python

# Grab the CGI module
import cgi

# Log any problems that we might have
import cgitb
cgitb.enable(display=0, logdir="/tmp")

# Where is our calendar file?
calendar_directory = '/usr/local/apache2/calendars/'
calendar_file = calendar_directory + 'test.ics'

# Send a content-type header to the user's browser
print "Content-type: text/calendar\n\n"

# Send the contents of the file  to the browser
calendar_filehandle = open(calendar_file, "rb")
print calendar_filehandle.read()
calendar_filehandle.close()

如果您以前没有用 Python 编写过 CGI 程序,那么这个示例应该演示它是多么简单。加载 CGI 模块以获得一些基本的 CGI 功能。然后,加载 cgitb,用于 CGI 回溯,模块,它允许我们在文件中放入调试信息,如果并且当问题发生时。

然后,我们发送一个 text/calendar Content-type 标头。可以肯定地假设,Web 上的大多数内容都是使用 text/html(用于 HTML 格式的文本)、text/plain(用于纯文本文件)的 Content-type 发送的,其中许多类型为 image/jpeg、image/png 和 image/gif。iCalendar 标准表明,与日历文件关联的适当 Content-type 是 text/calendar,即使像 Sunbird 这样的程序也足够宽容,可以接受 text/plain 格式。最后,我们通过发送日历文件的内容来结束程序,我们从本地文件系统中读取该内容。

如果您从事 Web 编程已经有一段时间了,那么这个示例应该会引起各种警惕。我们使用程序返回静态文件的想法似乎有点愚蠢,尽管这确实有一个小小的优势,即让我们能够向外部用户隐藏日历文件的真实位置。毫无疑问,有更好的方法可以实现这一点,包括 Apache Alias 指令。我们可以通过将日历的文件名作为参数传递来稍微改进这个程序,但这仍然需要我们拥有一组静态生成的文件。

创建 iCalendar

真正的解决方案,也是让生活更有趣的解决方案,是在用户请求时动态创建 iCalendar 文件。也就是说,我们的 CGI 程序不返回现有 iCalendar 文件的内容;相反,它以编程方式创建一个 iCalendar 文件,并将其返回给用户的日历客户端程序。

乍一看,这似乎是一项简单的任务。毕竟,iCalendar 文件格式看起来很简单,所以也许我们可以自己编写一些代码。但是,经过仔细检查,我们发现创建 iCalendar 文件说起来容易做起来难,特别是当我们想要包含重复事件时。

鉴于 iCalendar 标准日益普及以及大量开源项目,我很惊讶地发现 iCalendar 受到了最大的开源编程社区的相对较少的关注。我感到惊讶的部分原因是 iCalendar 已经存在多年,被许多公司使用,并受到许多日历程序的支持,从 Novell 的 Evolution 到 Lotus Notes 再到 Microsoft Outlook。这种组合通常是多种不同选择的秘诀,使用多种不同的编程语言。

我首先查看了 Perl,它的 CPAN 存档以其许多模块而闻名,包括许多用于各种 Internet 标准的模块。虽然有几个 Perl 模块可用于解析 iCalendar 文件,但没有最新的模块可用于构建它们。Net::ICal::Libical 本来要成为 C 语言 libical 库的包装器,但最后一次发布是在几年前的 pre-alpha 版本中。Net::ICal 是一个名为 ReefKnot 的项目的一部分,该项目也似乎已被放弃。

幸运的是,丹麦开发人员 Max M(请参阅在线资源)最近决定填补这一空白,并编写了一个 Python 包,可以轻松创建 iCalendar 文件。我在我的计算机上下载并安装了该软件包,没有任何问题,我发现使用该软件包创建日历非常简单。结合我们之前的简单 CGI 程序,我们应该能够毫无问题地创建和发布日历。

创建动态日历

我从 maxm.dk 站点下载并安装了 iCalendar 包。与许多现代 Python 包不同,它不会自动安装。您必须手动将其复制到系统的 site-packages 目录,在我的 Fedora Core 3 系统上,该目录位于 /usr/lib/python-2.3/site-packages。

正如您在列表 2 中看到的,我能够使用这个新安装的 iCalendar 包来创建 Calendar 和 Event 类型的新对象。我要做的第一件事是将适当的包导入到当前命名空间中

from iCalendar import Calendar, Event

列表 2. dynamic-calendar.py,一个以 iCalendar 格式生成日历的程序。

#!/usr/bin/python

# Grab the CGI module
import cgi
from iCalendar import Calendar, Event
from datetime import datetime
from iCalendar import UTC # timezone

# Log any problems that we might have
import cgitb
cgitb.enable(display=0, logdir="/tmp")

# Send a content-type header to the user's browser
print "Content-type: text/calendar\n\n"

# Create a calendar object
cal = Calendar()

# What product created the calendar?
cal.add('prodid',
        '-//Python iCalendar 0.9.3//mxm.dk//')

# Version 2.0 corresponds to RFC 2445
cal.add('version', '2.0')

# Create one event
event = Event()
event.add('summary', 'ATF deadline')
event.add('dtstart',
          datetime(2005,3,11,8,0,0,tzinfo=UTC()))
event.add('dtend',
          datetime(2005,3,11,10,0,0,tzinfo=UTC()))
event.add('dtstamp',
          datetime(2005,3,11,0,10,0,tzinfo=UTC()))
event['uid'] = 'ATF20050311A@lerner.co.il'

# Give this very high priority!
event.add('priority', 5)

# Add the event to the calendar
cal.add_component(event)

# Ask the calendar to render itself as an iCalendar
# file, and return that file in an HTTP response!
print cal.as_string()

iCalendar 包中的 Calendar 和 Event 模块分别对应于整个 iCalendar 文件和该文件中的一个事件。因此,我们为我们可能想要创建的每个事件创建一个 Calendar 对象的单个实例和一个 Event 对象。

然后我们可以创建日历对象

cal = Calendar()
cal.add('prodid',
        '-//Python iCalendar 0.9.3//mxm.dk//')
cal.add('version', '2.0')

这里的第二行和第三行,我们调用 cal.add(),允许我们向 iCalendar 文件添加识别数据。其中第一个允许我们告诉客户端软件哪个程序生成了 iCalendar 文件。这对于调试很有用;如果我们始终从特定的软件包获得损坏的 iCalendar 文件,我们可以联系作者或发布者并报告错误。第二行,我们添加版本标识符,指示我们遵循哪个版本的 iCalendar 规范。RFC 2445 指示,如果我们打算遵循该规范,我们应该为此字段赋值 2.0。

现在我们已经创建了一个日历,让我们创建一个事件并为其提供一个摘要行,以显示在订阅此 iCalendar 文件的任何人的日历程序中

event = Event()
event.add('summary', 'ATF deadline')

正如我们已经在检查的文件中看到的那样,每个事件都有三个与之关联的日期/时间字段:开始日期和时间 dtstart;结束日期和时间 dtend;以及指示此条目何时添加到日历的 dtstamp。iCalendar 标准为其日期和时间使用了一种奇怪但有用的格式,但是 Event 对象知道如何处理这些格式,如果我们给它一个来自标准 datetime Python 包的 datetime 对象。所以,我们可以说

event.add('dtstart',
          datetime(2005,3,11,14,0,0,tzinfo=UTC()))
event.add('dtend',
          datetime(2005,3,11,16,0,0,tzinfo=UTC()))
event.add('dtstamp',
          datetime(2005,3,11,0,10,0,tzinfo=UTC()))

请注意,以上三行使用 UTC 作为时区。当 iCalendar 文件显示在客户端日历应用程序中时,它会以用户的本地时区显示,而不是 UTC。

创建事件后,我们需要为其提供唯一的 ID。当我说唯一时,我的意思是 ID 应该是真正唯一的,在世界上所有日历和计算机中都是唯一的。这听起来比实际情况更棘手。您可以使用许多不同的策略,包括使用创建时间戳、创建事件的计算机的 IP 地址和一个大的随机数的组合。我决定创建一个简单的 UID,但是如果您正在创建一个要在多台计算机之间共享的应用程序,您可能应该考虑要创建哪种 UID,然后对其进行标准化

event['uid'] = 'ATF20050311A@lerner.co.il'

最后,我们必须给我们的事件一个优先级,范围从 0 到 9。优先级为 5 的事件被认为是正常或平均的;紧急项目获得更高的数字,不太紧急的项目获得更低的数字

event.add('priority', 5)

创建事件后,我们将其附加到日历对象,该对象一直在等待我们对其进行处理

cal.add_component(event)

如果我们有兴趣,我们可以向日历添加更多事件。只要每个事件都有唯一的 UID 字段,就不会有任何问题。

最后,我们使用 as_string() 方法将我们的 Calendar 对象转换为 iCalendar 文件

print cal.as_string()

因为 print 默认写入标准输出,并且因为 CGI 程序将其标准输出发送回 HTTP 客户端,这具有将 iCalendar 文件发送回发出 HTTP 请求的任何人的效果。并且因为我们已将 MIME 类型定义为 text/calendar 类型,所以 HTTP 客户端知道将其解释为日历并适当地显示它。如果我们自己查看输出,我们会看到它确实是 iCalendar 格式

BEGIN:VCALENDAR
PRODID:-//Python iCalendar 0.9.3//mxm.dk//
VERSION:2.0
BEGIN:VEVENT
DTEND:20050311T160000Z
DTSTAMP:20050311T001000Z
DTSTART:20050311T140000Z
PRIORITY:5
SUMMARY:ATF deadline
UID:ATF20050311A@lerner.co.il
END:VEVENT
END:VCALENDAR

现在,我必须承认这个例子几乎和前一个例子一样牵强。诚然,我们已经利用了我们可以动态生成日历的事实,但是此事件被硬编码到程序中,使得非程序员无法添加、修改或删除事件。也就是说,我们已经朝着事件和日期的程序化计算迈出了又一步。下一步是将日期存储在文件甚至关系数据库中,并使用我们的程序来动态转换信息。

结论

本月,我们研究了使用 Python 的 iCalendar 模块在简单的 CGI 程序中包装的动态日历的创建。与此同时,我们看到了拥有日历的局限性,日历条目需要存储在磁盘上。更好的解决方案是将事件信息放入关系数据库中,该数据库内置了对日期的支持,以及用于用户和组访问的安全机制。下个月,我们将扩展我们的日历程序,使其从数据库中检索信息,将 PostgreSQL 表转换为 iCalendar 文件。

本文的资源: /article/8197

Reuven M. Lerner,一位长期的 Web/数据库顾问和开发人员,现在是西北大学学习科学项目的研究生。他的博客位于 altneuland.lerner.co.il,您可以通过 reuven@lerner.co.il 与他联系。

加载 Disqus 评论