6 commits - po/en.po pykolab/itip pykolab/translate.py pykolab/xml tests/functional tests/unit wallace/module_invitationpolicy.py wallace/module_resources.py
Thomas Brüderli
bruederli at kolabsys.com
Tue Jul 22 13:58:47 CEST 2014
po/en.po | 15 -
pykolab/itip/__init__.py | 7
pykolab/translate.py | 14 -
pykolab/xml/__init__.py | 1
pykolab/xml/attendee.py | 21 ++
pykolab/xml/event.py | 3
tests/functional/test_wallace/test_005_resource_invitation.py | 56 +++---
tests/functional/test_wallace/test_007_invitationpolicy.py | 48 ++++-
tests/unit/test-002-attendee.py | 7
tests/unit/test-011-itip.py | 6
tests/unit/test-012-wallace_invitationpolicy.py | 16 +
tests/unit/test-015-translate.py | 10 -
wallace/module_invitationpolicy.py | 82 +++++++++-
wallace/module_resources.py | 7
14 files changed, 209 insertions(+), 84 deletions(-)
New commits:
commit 5d97d7f2da3d97930941c081c8bbba7de687863f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 10 06:26:46 2014 -0400
Implement gettext language switch; remove en.po as this is not used
diff --git a/po/en.po b/po/en.po
deleted file mode 100644
index da6a905..0000000
--- a/po/en.po
+++ /dev/null
@@ -1,15 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Kolab Groupware Solution\n"
-"Report-Msgid-Bugs-To: https://isues.kolab.org/\n"
-"POT-Creation-Date: 2014-07-17 10:22+0100\n"
-"PO-Revision-Date: 2014-07-14 11:13+0000\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: en\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
diff --git a/pykolab/translate.py b/pykolab/translate.py
index 85f4516..080cbc2 100644
--- a/pykolab/translate.py
+++ b/pykolab/translate.py
@@ -26,9 +26,10 @@ import gettext
import os
N_ = lambda x: x
-_ = lambda x: gettext.ldgettext(domain, x)
+_ = lambda x: current.lgettext(x)
-#gettext.bindtextdomain(domain, '/usr/local/share/locale')
+localedir = '/usr/local/share/locale'
+current = gettext.translation(domain, localedir, fallback=True)
def getDefaultLangs():
languages = []
@@ -49,15 +50,14 @@ def getDefaultLangs():
return nelangs
def setUserLanguage(lang):
+ global current
+
langs = []
for l in gettext._expand_lang(lang):
if l not in langs:
langs.append(l)
try:
- translation = gettext.translation(domain, languages=langs)
- translation.install()
+ current = gettext.translation(domain, localedir, languages=langs, fallback=True)
except:
- return False
-
- return True
+ pass
diff --git a/tests/unit/test-015-translate.py b/tests/unit/test-015-translate.py
index 8ca9463..6819b80 100644
--- a/tests/unit/test-015-translate.py
+++ b/tests/unit/test-015-translate.py
@@ -4,9 +4,6 @@ from pykolab import translate
class TestTranslate(unittest.TestCase):
- def setUp(self):
- translate.setUserLanguage('en')
-
def test_001_default_langs(self):
self.assertTrue(len(translate.getDefaultLangs()) > 0)
@@ -16,10 +13,11 @@ class TestTranslate(unittest.TestCase):
def test_003_set_lang(self):
from pykolab.translate import _
- self.assertFalse(translate.setUserLanguage('foo_bar'))
self.assertEqual(_("Folder name"), "Folder name")
- self.assertTrue(translate.setUserLanguage('de_DE'))
- self.assertEqual(_("Folder name"), "Ordnername")
+ translate.setUserLanguage('de_DE')
+ self.assertEqual(_("Folder name"), "Ordnername", "German Translation found")
+ translate.setUserLanguage('foo_bar')
+ self.assertEqual(_("Folder name"), "Folder name", "Unkonwn language falls back to NullTranslations")
if __name__ == '__main__':
unittest.main()
commit 6b3df45c2cc13f5fcc3320404881cbff0a8f012e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 10 05:17:01 2014 -0400
Use deferred translations
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 220ab8c..a01d955 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -1,27 +1,28 @@
import kolabformat
from pykolab.translate import _
+from pykolab.translate import N_
from contact_reference import ContactReference
participant_status_labels = {
- "NEEDS-ACTION": _("Needs Action"),
- "ACCEPTED": _("Accepted"),
- "DECLINED": _("Declined"),
- "TENTATIVE": _("Tentatively Accepted"),
- "DELEGATED": _("Delegated"),
- "COMPLETED": _("Completed"),
- "IN-PROCESS": _("In Process"),
+ "NEEDS-ACTION": N_("Needs Action"),
+ "ACCEPTED": N_("Accepted"),
+ "DECLINED": N_("Declined"),
+ "TENTATIVE": N_("Tentatively Accepted"),
+ "DELEGATED": N_("Delegated"),
+ "COMPLETED": N_("Completed"),
+ "IN-PROCESS": N_("In Process"),
# support integer values, too
- kolabformat.PartNeedsAction: _("Needs Action"),
- kolabformat.PartAccepted: _("Accepted"),
- kolabformat.PartDeclined: _("Declined"),
- kolabformat.PartTentative: _("Tentatively Accepted"),
- kolabformat.PartDelegated: _("Delegated"),
+ kolabformat.PartNeedsAction: N_("Needs Action"),
+ kolabformat.PartAccepted: N_("Accepted"),
+ kolabformat.PartDeclined: N_("Declined"),
+ kolabformat.PartTentative: N_("Tentatively Accepted"),
+ kolabformat.PartDelegated: N_("Delegated"),
}
def participant_status_label(status):
- return participant_status_labels[status] if participant_status_labels.has_key(status) else status
+ return _(participant_status_labels[status]) if participant_status_labels.has_key(status) else status
class Attendee(kolabformat.Attendee):
commit c395789e553531a4a565bcc61a422255ef5385b4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 10 05:09:11 2014 -0400
Send consolidated update notifications to an event organizer. This means suppressing notifications triggered by wallace replies as long as more automated replies can be expected; Use localized participant status texts in iTip messages
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 8dc3ff7..dd419f0 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -12,6 +12,7 @@ from wallace import module_resources
from pykolab.translate import _
from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
from email import message_from_string
from twisted.trial import unittest
@@ -177,6 +178,16 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
}
+ self.bob = {
+ 'displayname': 'Bob Auto',
+ 'mail': 'bob.auto at example.org',
+ 'dn': 'uid=auto,ou=People,dc=example,dc=org',
+ 'preferredlanguage': 'en_US',
+ 'mailbox': 'user/bob.auto at example.org',
+ 'kolabtargetfolder': 'user/bob.auto/Calendar at example.org',
+ 'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE']
+ }
+
self.external = {
'displayname': 'Bob External',
'mail': 'bob.external at gmail.com'
@@ -186,6 +197,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'], preferredlanguage=self.john['preferredlanguage'])
user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'], preferredlanguage=self.jane['preferredlanguage'])
user_add("Jack", "Tentative", kolabinvitationpolicy=self.jack['kolabinvitationpolicy'], preferredlanguage=self.jack['preferredlanguage'])
+ user_add("Bob", "Auto", kolabinvitationpolicy=self.bob['kolabinvitationpolicy'], preferredlanguage=self.bob['preferredlanguage'])
time.sleep(1)
from tests.functional.synchronize import synchronize_once
@@ -432,7 +444,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
start = datetime.datetime(2014,8,13, 10,0,0)
uid = self.send_itip_invitation(self.jane['mail'], start)
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -453,7 +465,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
def test_002_invite_conflict_reject(self):
uid = self.send_itip_invitation(self.jane['mail'], datetime.datetime(2014,8,13, 11,0,0), summary="test2")
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':_('DECLINED') }, self.jane['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -466,7 +478,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,24, 8,0,0))
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
self.assertIsInstance(response, email.message.Message)
@@ -474,12 +486,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.purge_mailbox(self.john['mailbox'])
self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 8,0,0))
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
self.assertIsInstance(response, email.message.Message)
# send conflicting request to jack
uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 10,0,0), summary="test2")
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':_('DECLINED') }, self.jack['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jack['mail'])
self.assertEqual(response, None, "No reply expected")
event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
@@ -494,7 +506,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
start = datetime.datetime(2014,8,14, 9,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
uid = self.send_itip_invitation(self.jane['mail'], start)
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -507,7 +519,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
new_start = datetime.datetime(2014,8,15, 15,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
self.send_itip_update(self.jane['mail'], uid, new_start, summary="test", sequence=1)
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -523,7 +535,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
uid = self.send_itip_invitation(self.jack['mail'], start)
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
self.assertIsInstance(response, email.message.Message)
# send update with new but conflicting date and incremented sequence
@@ -531,7 +543,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
new_start = datetime.datetime(2014,8,10, 9,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1)
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('DECLINED') }, self.jack['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.jack['mail'])
self.assertEqual(response, None)
# verify re-scheduled copy in jack's calendar with NEEDS-ACTION
@@ -577,7 +589,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
uid = self.send_itip_invitation(self.jane['mail'], summary="cancelled")
- response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':_('ACCEPTED') }, self.jane['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
self.assertIsInstance(response, email.message.Message)
self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
@@ -594,13 +606,19 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.purge_mailbox(self.john['mailbox'])
start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
- uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack])
+ uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.bob, self.jack])
# send a reply from jane to john
self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start)
# check for notification message
- # TODO: this notification should be suppressed until jack has replied, too
+ # this notification should be suppressed until bob has replied, too
+ notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+ self.assertEqual(notification, None)
+
+ # send a reply from bob to john
+ self.send_itip_reply(uid, self.bob['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
+
notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
self.assertIsInstance(notification, email.message.Message)
@@ -610,14 +628,14 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.purge_mailbox(self.john['mailbox'])
- # send a reply from jack to john
- self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='TENTATIVE')
+ # send a reply from bob to john
+ self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
+ # this triggers an additional notification
notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
self.assertIsInstance(notification, email.message.Message)
notification_text = str(notification.get_payload());
- self.assertIn(self.jack['mail'], notification_text)
self.assertNotIn(_("PENDING"), notification_text)
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
index 0b64f6a..dbe0713 100644
--- a/tests/unit/test-012-wallace_invitationpolicy.py
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -142,4 +142,20 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
MIP.remove_write_lock(lock_key)
self.assertFalse(os.path.isfile(lock_file))
+ def test_005_is_auto_reply(self):
+ all_manual = [ 'ACT_MANUAL' ]
+ accept_none = [ 'ACT_REJECT' ]
+ accept_all = [ 'ACT_ACCEPT', 'ACT_UPDATE' ]
+ accept_cond = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT' ]
+ accept_some = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_SAVE_TO_CALENDAR:example.org', 'ACT_REJECT_IF_CONFLICT' ]
+ accept_avail = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT:example.org' ]
+
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual }, 'domain.org'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none }, 'domain.org'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all }, 'domain.com'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond }, 'domain.com'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'domain.com'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'example.org'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'domain.com'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'example.org'))
\ No newline at end of file
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 1426e2b..03585ee 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -41,6 +41,7 @@ from pykolab.conf import Conf
from pykolab.imap import IMAP
from pykolab.xml import to_dt
from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
from pykolab.itip import events_from_message
from pykolab.itip import check_event_conflict
from pykolab.itip import send_reply
@@ -500,12 +501,17 @@ def user_dn_from_email_address(email_address):
auth = Auth()
auth.connect()
+ # return cached value
+ if user_dn_from_email_address.cache.has_key(email_address):
+ return user_dn_from_email_address.cache[email_address]
+
local_domains = auth.list_domains()
if not local_domains == None:
local_domains = list(set(local_domains.keys()))
if not email_address.split('@')[1] in local_domains:
+ user_dn_from_email_address.cache[email_address] = None
return None
log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8)
@@ -517,8 +523,13 @@ def user_dn_from_email_address(email_address):
else:
log.debug(_("No user record(s) found for %r") % (email_address), level=9)
+ # remember this lookup
+ user_dn_from_email_address.cache[email_address] = user_dn
+
return user_dn
+user_dn_from_email_address.cache = {}
+
def get_matching_invitation_policies(receiving_user, sender_domain):
# get user's kolabInvitationPolicy settings
@@ -843,6 +854,8 @@ def send_reply_notification(event, receiving_user):
"""
Send a (consolidated) notification about the current participant status to organizer
"""
+ global auth
+
import smtplib
from email.MIMEText import MIMEText
from email.Utils import formatdate
@@ -851,6 +864,13 @@ def send_reply_notification(event, receiving_user):
event.uid, receiving_user['mail']
), level=8)
+ organizer = event.get_organizer()
+ orgemail = organizer.email()
+ orgname = organizer.name()
+ sender_domain = orgemail.split('@')[-1]
+
+ auto_replies_expected = 0
+ auto_replies_received = 0
partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] }
for attendee in event.get_attendees():
parstat = attendee.get_participant_status(True)
@@ -859,13 +879,34 @@ def send_reply_notification(event, receiving_user):
else:
partstats['PENDING'].append(attendee.get_displayname())
- # TODO: for every attendee, look-up its kolabinvitationpolicy and skip notification
- # until we got replies from all automatically responding attendees
+ # look-up kolabinvitationpolicy for this attendee
+ if attendee.get_cutype() == kolabformat.CutypeResource:
+ resource_dns = auth.find_resource(attendee.get_email())
+ if isinstance(resource_dns, list):
+ attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
+ else:
+ attendee_dn = resource_dns
+ else:
+ attendee_dn = user_dn_from_email_address(attendee.get_email())
+
+ if attendee_dn:
+ attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
+ if is_auto_reply(attendee_rec, sender_domain):
+ auto_replies_expected += 1
+ if not parstat == 'NEEDS-ACTION':
+ auto_replies_received += 1
+
+ # skip notification until we got replies from all automatically responding attendees
+ if auto_replies_received < auto_replies_expected:
+ log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % (
+ auto_replies_received, auto_replies_expected
+ ), level=8)
+ return
roundup = ''
for status,attendees in partstats.iteritems():
if len(attendees) > 0:
- roundup += "\n" + _(status) + ":\n" + "\n".join(attendees) + "\n"
+ roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
message_text = """
The event '%(summary)s' at %(start)s has been updated in your calendar.
@@ -882,11 +923,6 @@ def send_reply_notification(event, receiving_user):
msg['To'] = receiving_user['mail']
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = _('"%s" has been updated') % (event.get_summary())
-
- organizer = event.get_organizer()
- orgemail = organizer.email()
- orgname = organizer.name()
-
msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
smtp = smtplib.SMTP("localhost", 10027)
@@ -902,6 +938,36 @@ def send_reply_notification(event, receiving_user):
smtp.quit()
+def is_auto_reply(user, sender_domain):
+ accept_available = False
+ accept_conflicts = False
+ for policy in get_matching_invitation_policies(user, sender_domain):
+ if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE):
+ if check_policy_condition(policy, True):
+ accept_available = True
+ if check_policy_condition(policy, False):
+ accept_conflicts = True
+
+ # we have both cases covered by a policy
+ if accept_available and accept_conflicts:
+ return True
+
+ # manual action reached
+ if policy & (ACT_MANUAL | ACT_SAVE_TO_CALENDAR):
+ return False
+
+ return False
+
+
+def check_policy_condition(policy, available):
+ condition_fulfilled = True
+ if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
+ condition_fulfilled = available
+ if policy & COND_IF_CONFLICT:
+ condition_fulfilled = not condition_fulfilled
+ return condition_fulfilled
+
+
def propagate_changes_to_attendees_calendars(event):
"""
Find and update copies of this event in all attendee's calendars
commit d13dd3849d09d1eaa4fdd906686cb1217cc23ad4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 10 05:06:42 2014 -0400
Use localized participant status texts in resource replies
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 79ceaf2..60b6587 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -8,7 +8,9 @@ import uuid
from pykolab.imap import IMAP
from wallace import module_resources
+from pykolab.translate import _
from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
from email import message_from_string
from twisted.trial import unittest
@@ -185,6 +187,8 @@ class TestResourceInvitation(unittest.TestCase):
@classmethod
def setup_class(self, *args, **kw):
+ self.itip_reply_subject = _("Reservation Request for %(summary)s was %(status)s")
+
from tests.functional.purge_users import purge_users
purge_users()
@@ -373,7 +377,7 @@ class TestResourceInvitation(unittest.TestCase):
def test_002_invite_resource(self):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 10,0,0))
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -384,7 +388,7 @@ class TestResourceInvitation(unittest.TestCase):
def test_003_invite_resource_conflict(self):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 12,0,0))
- response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
self.assertEqual(self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid), None)
@@ -396,7 +400,7 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,7,13, 12,0,0))
# one of the collection members accepted the reservation
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
delegatee = self.find_resource_by_email(accept['from'])
@@ -406,7 +410,7 @@ class TestResourceInvitation(unittest.TestCase):
self.assertIsInstance(self.check_resource_calendar_event(delegatee['kolabtargetfolder'], uid), pykolab.xml.Event)
# resource collection responds with a DELEGATED message
- response = self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail'])
self.assertIsInstance(response, email.message.Message)
self.assertIn("ROLE=NON-PARTICIPANT;RSVP=FALSE", str(response))
@@ -416,13 +420,13 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,4,1, 10,0,0))
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
self.purge_mailbox(self.john['mailbox'])
self.send_itip_update(self.audi['mail'], uid, datetime.datetime(2014,4,1, 12,0,0)) # conflict with myself
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -437,13 +441,13 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,4,24, 12,0,0))
# one of the collection members accepted the reservation
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
delegatee = self.find_resource_by_email(accept['from'])
# book that resource for the next day
self.send_itip_invitation(delegatee['mail'], datetime.datetime(2014,4,25, 14,0,0))
- accept2 = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept2 = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
# re-schedule first booking to a conflicting date
self.purge_mailbox(self.john['mailbox'])
@@ -451,7 +455,7 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_update(delegatee['mail'], uid, datetime.datetime(2014,4,25, 12,0,0), template=update_template)
# expect response from another member of the initially delegated collection
- new_accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ new_accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(new_accept, email.message.Message)
new_delegatee = self.find_resource_by_email(new_accept['from'])
@@ -462,7 +466,7 @@ class TestResourceInvitation(unittest.TestCase):
self.assertIsInstance(event, pykolab.xml.Event)
# old resource responds with a DELEGATED message
- response = self.check_message_received("Reservation Request for test was DELEGATED", delegatee['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, delegatee['mail'])
self.assertIsInstance(response, email.message.Message)
# old reservation was removed from old delegate's calendar
@@ -483,7 +487,7 @@ class TestResourceInvitation(unittest.TestCase):
# make new reservation to the now free'd slot
self.send_itip_invitation(self.boxter['mail'], datetime.datetime(2014,5,1, 9,0,0))
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.boxter['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.boxter['mail'])
self.assertIsInstance(response, email.message.Message)
@@ -494,7 +498,7 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.cars['mail'], dt)
# wait for accept notification
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
delegatee = self.find_resource_by_email(accept['from'])
@@ -505,12 +509,12 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_update(delegatee['mail'], uid, dt, template=update_template)
# get response from delegatee
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
self.assertIn(delegatee['mail'], accept['from'])
# no delegation response on updates
- self.assertEqual(self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail']), None)
+ self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail']), None)
def test_008_allday_reservation(self):
@@ -518,7 +522,7 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2), True)
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -526,7 +530,7 @@ class TestResourceInvitation(unittest.TestCase):
self.assertIsInstance(event.get_start(), datetime.date)
uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2, 16,0,0))
- response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
@@ -537,19 +541,19 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,20, 12,0,0),
template=itip_recurring.replace(";COUNT=10", ""))
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
# check non-recurring against recurring
uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,13, 10,0,0))
- response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
self.purge_mailbox(self.john['mailbox'])
# check recurring against recurring
uid3 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,22, 8,0,0), template=itip_recurring)
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
@@ -564,7 +568,7 @@ class TestResourceInvitation(unittest.TestCase):
itip_invalid = itip_invitation.replace("DTSTART;", "X-DTSTART;")
self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,24, 19,30,0), template=itip_invalid)
- self.assertEqual(self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail']), None)
+ self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail']), None)
def test_011_owner_info(self):
@@ -572,7 +576,7 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,6,19, 16,0,0))
- accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail'])
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room1['mail'])
self.assertIsInstance(accept, email.message.Message)
respose_text = str(accept.get_payload(0))
self.assertIn(self.jane['mail'], respose_text)
@@ -584,7 +588,7 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_invitation(self.room2['mail'], datetime.datetime(2014,6,19, 16,0,0))
- accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail'])
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room2['mail'])
self.assertIsInstance(accept, email.message.Message)
respose_text = str(accept.get_payload(0))
self.assertIn(self.jane['mail'], respose_text)
@@ -598,12 +602,12 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,8,4, 13,0,0))
# check notification message sent to resource owner (jane)
- notify = self.check_message_received("Booking for %s has been ACCEPTED" % (self.room1['cn']), self.room1['mail'], self.jane['mailbox'])
+ notify = self.check_message_received(_('Booking for %s has been %s') % (self.room1['cn'], participant_status_label('ACCEPTED')), self.room1['mail'], self.jane['mailbox'])
self.assertIsInstance(notify, email.message.Message)
notification_text = str(notify.get_payload())
self.assertIn(self.john['mail'], notification_text)
- self.assertIn("ACCEPTED", notification_text)
+ self.assertIn(participant_status_label('ACCEPTED'), notification_text)
self.purge_mailbox(self.john['mailbox'])
@@ -611,9 +615,9 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,8,4, 12,30,0))
# one of the collection members accepted the reservation
- accepted = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accepted = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
delegatee = self.find_resource_by_email(accepted['from'])
- notify = self.check_message_received("Booking for %s has been ACCEPTED" % (delegatee['cn']), delegatee['mail'], self.jane['mailbox'])
+ notify = self.check_message_received(_('Booking for %s has been %s') % (delegatee['cn'], participant_status_label('ACCEPTED')), delegatee['mail'], self.jane['mailbox'])
self.assertIsInstance(notify, email.message.Message)
self.assertIn(self.john['mail'], notification_text)
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index b99cf1a..dba2653 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -42,6 +42,7 @@ from pykolab.conf import Conf
from pykolab.imap import IMAP
from pykolab.xml import to_dt
from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
from pykolab.itip import events_from_message
from pykolab.itip import check_event_conflict
from pykolab.translate import _
@@ -905,7 +906,7 @@ def reservation_response_text(status, owner):
*** This is an automated response, please do not reply! ***
We hereby inform you that your reservation was %s.
- """) % (_(status))
+ """) % (participant_status_label(status))
if owner:
message_text += _("""
@@ -950,7 +951,7 @@ def send_owner_notification(resource, owner, itip_event, success=True):
msg['To'] = owner['mail']
msg['From'] = resource['mail']
msg['Date'] = formatdate(localtime=True)
- msg['Subject'] = _('Booking for %s has been %s') % (resource['cn'], _(status) if success else _('failed'))
+ msg['Subject'] = _('Booking for %s has been %s') % (resource['cn'], participant_status_label(status) if success else _('failed'))
smtp = smtplib.SMTP("localhost", 10027)
@@ -986,7 +987,7 @@ def owner_notification_text(resource, owner, event, success):
'resource': resource['cn'],
'summary': event.get_summary(),
'date': event.get_date_text(),
- 'status': _(status),
+ 'status': participant_status_label(status),
'orgname': organizer.name(),
'orgemail': organizer.email()
}
commit a44dcfb0690305fe42f3b374cef0fbced465d64d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 10 05:00:39 2014 -0400
Fix all-day event date checks
diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 8cf6435..17da24e 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -3,6 +3,7 @@ import pykolab
from pykolab.xml import to_dt
from pykolab.xml import event_from_ical
+from pykolab.xml import participant_status_label
from pykolab.translate import _
log = pykolab.getLogger('pykolab.wallace')
@@ -144,7 +145,7 @@ def check_event_conflict(kolab_event, itip_event):
return conflict
_es = to_dt(kolab_event.get_start())
- _ee = to_dt(kolab_event.get_end())
+ _ee = to_dt(kolab_event.get_ical_dtend()) # use iCal style end date: next day for all-day events
# naive loops to check for collisions in (recurring) events
# TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday)
@@ -208,10 +209,10 @@ def send_reply(from_address, itip_events, response_text, subject=None):
participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
event_summary = itip_event['xml'].get_summary()
- message_text = response_text % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }
+ message_text = response_text % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() }
if subject is not None:
- subject = subject % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }
+ subject = subject % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() }
try:
message = itip_event['xml'].to_message_itip(from_address,
diff --git a/tests/unit/test-011-itip.py b/tests/unit/test-011-itip.py
index abbaa92..a120fd2 100644
--- a/tests/unit/test-011-itip.py
+++ b/tests/unit/test-011-itip.py
@@ -357,6 +357,12 @@ class TestITip(unittest.TestCase):
event.set_uid(itip_event['uid'])
self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict for same UID")
+ allday = Event()
+ allday.set_start(datetime.date(2012,7,13))
+ allday.set_end(datetime.date(2012,7,13))
+
+ self.assertTrue(itip.check_event_conflict(allday, itip_event), "Conflicting allday event")
+
event2 = Event()
event2.set_start(datetime.datetime(2012,7,13, 10,0,0, tzinfo=pytz.timezone("US/Central")))
event2.set_end(datetime.datetime(2012,7,13, 11,0,0, tzinfo=pytz.timezone("US/Central")))
commit 214bde4656f3e3a10096ba54dfa2704f4e8de719
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 10 04:35:36 2014 -0400
New function to get a localized string for iCal participant status
diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 64b06ae..2bb42d4 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -1,5 +1,6 @@
from attendee import Attendee
from attendee import InvalidAttendeeParticipantStatusError
+from attendee import participant_status_label
from contact import Contact
from contact_reference import ContactReference
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 579158e..220ab8c 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -4,6 +4,26 @@ from pykolab.translate import _
from contact_reference import ContactReference
+participant_status_labels = {
+ "NEEDS-ACTION": _("Needs Action"),
+ "ACCEPTED": _("Accepted"),
+ "DECLINED": _("Declined"),
+ "TENTATIVE": _("Tentatively Accepted"),
+ "DELEGATED": _("Delegated"),
+ "COMPLETED": _("Completed"),
+ "IN-PROCESS": _("In Process"),
+ # support integer values, too
+ kolabformat.PartNeedsAction: _("Needs Action"),
+ kolabformat.PartAccepted: _("Accepted"),
+ kolabformat.PartDeclined: _("Declined"),
+ kolabformat.PartTentative: _("Tentatively Accepted"),
+ kolabformat.PartDelegated: _("Delegated"),
+ }
+
+def participant_status_label(status):
+ return participant_status_labels[status] if participant_status_labels.has_key(status) else status
+
+
class Attendee(kolabformat.Attendee):
cutype_map = {
"INDIVIDUAL": kolabformat.CutypeIndividual,
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 98a91aa..4ac4997 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -12,6 +12,7 @@ import pykolab
from pykolab import constants
from pykolab import utils
from pykolab.xml import utils as xmlutils
+from pykolab.xml import participant_status_label
from pykolab.translate import _
from os import path
@@ -928,7 +929,7 @@ class Event(object):
msg['Date'] = formatdate(localtime=True)
if subject is None:
- subject = _("Invitation for %s was %s") % (self.get_summary(), _(participant_status))
+ subject = _("Invitation for %s was %s") % (self.get_summary(), participant_status_label(participant_status))
msg["Subject"] = subject
diff --git a/tests/unit/test-002-attendee.py b/tests/unit/test-002-attendee.py
index 9da93c7..8bcee3c 100644
--- a/tests/unit/test-002-attendee.py
+++ b/tests/unit/test-002-attendee.py
@@ -1,7 +1,9 @@
import datetime
import unittest
+import kolabformat
from pykolab.xml import Attendee
+from pykolab.xml import participant_status_label
class TestEventXML(unittest.TestCase):
attendee = Attendee("jane at doe.org")
@@ -101,5 +103,10 @@ class TestEventXML(unittest.TestCase):
self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 2][0], "INDIVIDUAL")
self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 3][0], "RESOURCE")
+ def test_018_partstat_label(self):
+ self.assertEqual(participant_status_label('NEEDS-ACTION'), "Needs Action")
+ self.assertEqual(participant_status_label(kolabformat.PartTentative), "Tentatively Accepted")
+ self.assertEqual(participant_status_label('UNKNOWN'), "UNKNOWN")
+
if __name__ == '__main__':
unittest.main()
More information about the commits
mailing list