1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imipweb/event.py Thu Mar 26 16:11:46 2015 +0100
1.3 @@ -0,0 +1,1060 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +A Web interface to a calendar event.
1.8 +
1.9 +Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
1.10 +
1.11 +This program is free software; you can redistribute it and/or modify it under
1.12 +the terms of the GNU General Public License as published by the Free Software
1.13 +Foundation; either version 3 of the License, or (at your option) any later
1.14 +version.
1.15 +
1.16 +This program is distributed in the hope that it will be useful, but WITHOUT
1.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
1.19 +details.
1.20 +
1.21 +You should have received a copy of the GNU General Public License along with
1.22 +this program. If not, see <http://www.gnu.org/licenses/>.
1.23 +"""
1.24 +
1.25 +from datetime import datetime, timedelta
1.26 +from imiptools.client import update_attendees
1.27 +from imiptools.data import get_uri, uri_dict, uri_values
1.28 +from imiptools.dates import format_datetime, to_date, get_datetime, \
1.29 + get_datetime_item, get_period_item, \
1.30 + get_start_of_day, to_timezone
1.31 +from imiptools.mail import Messenger
1.32 +from imiptools.period import have_conflict
1.33 +from imipweb.handler import ManagerHandler
1.34 +from imipweb.resource import Resource
1.35 +import pytz
1.36 +
1.37 +class EventPage(Resource):
1.38 +
1.39 + "A request handler for the event page."
1.40 +
1.41 + def __init__(self, resource=None, messenger=None):
1.42 + Resource.__init__(self, resource)
1.43 + self.messenger = messenger or Messenger()
1.44 +
1.45 + # Request logic methods.
1.46 +
1.47 + def handle_request(self, uid, recurrenceid, obj):
1.48 +
1.49 + """
1.50 + Handle actions involving the given 'uid', 'recurrenceid', and 'obj' as
1.51 + the object's representation, returning an error if one occurred, or None
1.52 + if the request was successfully handled.
1.53 + """
1.54 +
1.55 + # Handle a submitted form.
1.56 +
1.57 + args = self.env.get_args()
1.58 +
1.59 + # Get the possible actions.
1.60 +
1.61 + reply = args.has_key("reply")
1.62 + discard = args.has_key("discard")
1.63 + invite = args.has_key("invite")
1.64 + cancel = args.has_key("cancel")
1.65 + save = args.has_key("save")
1.66 + ignore = args.has_key("ignore")
1.67 +
1.68 + have_action = reply or discard or invite or cancel or save or ignore
1.69 +
1.70 + if not have_action:
1.71 + return ["action"]
1.72 +
1.73 + # If ignoring the object, return to the calendar.
1.74 +
1.75 + if ignore:
1.76 + self.redirect(self.env.get_path())
1.77 + return None
1.78 +
1.79 + # Update the object.
1.80 +
1.81 + if args.has_key("summary"):
1.82 + obj["SUMMARY"] = [(args["summary"][0], {})]
1.83 +
1.84 + attendees = uri_dict(obj.get_value_map("ATTENDEE"))
1.85 +
1.86 + if args.has_key("partstat"):
1.87 + if attendees.has_key(self.user):
1.88 + attendees[self.user]["PARTSTAT"] = args["partstat"][0]
1.89 + if attendees[self.user].has_key("RSVP"):
1.90 + del attendees[self.user]["RSVP"]
1.91 +
1.92 + is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.93 +
1.94 + # Obtain the user's timezone and process datetime values.
1.95 +
1.96 + update = False
1.97 +
1.98 + if is_organiser:
1.99 + periods, errors = self.handle_all_period_controls()
1.100 + if errors:
1.101 + return errors
1.102 + elif periods:
1.103 + self.set_period_in_object(obj, periods[0])
1.104 + self.set_periods_in_object(obj, periods[1:])
1.105 +
1.106 + # Obtain any participants to be added or removed.
1.107 +
1.108 + removed = args.get("remove")
1.109 + added = args.get("added")
1.110 +
1.111 + # Process any action.
1.112 +
1.113 + handled = True
1.114 +
1.115 + if reply or invite or cancel:
1.116 +
1.117 + handler = ManagerHandler(obj, self.user, self.messenger)
1.118 +
1.119 + # Process the object and remove it from the list of requests.
1.120 +
1.121 + if reply and handler.process_received_request(update) or \
1.122 + is_organiser and (invite or cancel) and \
1.123 + handler.process_created_request(invite and "REQUEST" or "CANCEL", update, removed, added):
1.124 +
1.125 + self.remove_request(uid, recurrenceid)
1.126 +
1.127 + # Save single user events.
1.128 +
1.129 + elif save:
1.130 + to_cancel = update_attendees(obj, added, removed)
1.131 + self.store.set_event(self.user, uid, recurrenceid, node=obj.to_node())
1.132 + self.update_freebusy(uid, recurrenceid, obj)
1.133 + self.remove_request(uid, recurrenceid)
1.134 +
1.135 + # Remove the request and the object.
1.136 +
1.137 + elif discard:
1.138 + self.remove_from_freebusy(uid, recurrenceid)
1.139 + self.remove_event(uid, recurrenceid)
1.140 + self.remove_request(uid, recurrenceid)
1.141 +
1.142 + else:
1.143 + handled = False
1.144 +
1.145 + # Upon handling an action, redirect to the main page.
1.146 +
1.147 + if handled:
1.148 + self.redirect(self.env.get_path())
1.149 +
1.150 + return None
1.151 +
1.152 + def handle_all_period_controls(self):
1.153 +
1.154 + """
1.155 + Handle datetime controls for a particular period, where 'index' may be
1.156 + used to indicate a recurring period, or the main start and end datetimes
1.157 + are handled.
1.158 + """
1.159 +
1.160 + args = self.env.get_args()
1.161 +
1.162 + periods = []
1.163 +
1.164 + # Get the main period details.
1.165 +
1.166 + dtend_enabled = args.get("dtend-control", [None])[0]
1.167 + dttimes_enabled = args.get("dttimes-control", [None])[0]
1.168 + start_values = self.get_date_control_values("dtstart")
1.169 + end_values = self.get_date_control_values("dtend")
1.170 +
1.171 + period, errors = self.handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled)
1.172 +
1.173 + if errors:
1.174 + return None, errors
1.175 +
1.176 + periods.append(period)
1.177 +
1.178 + # Get the recurring period details.
1.179 +
1.180 + all_dtend_enabled = args.get("dtend-control-recur", [])
1.181 + all_dttimes_enabled = args.get("dttimes-control-recur", [])
1.182 + all_start_values = self.get_date_control_values("dtstart-recur", multiple=True)
1.183 + all_end_values = self.get_date_control_values("dtend-recur", multiple=True)
1.184 +
1.185 + for index, (start_values, end_values, dtend_enabled, dttimes_enabled) in \
1.186 + enumerate(map(None, all_start_values, all_end_values, all_dtend_enabled, all_dttimes_enabled)):
1.187 +
1.188 + dtend_enabled = str(index) in all_dtend_enabled
1.189 + dttimes_enabled = str(index) in all_dttimes_enabled
1.190 + period, errors = self.handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled)
1.191 +
1.192 + if errors:
1.193 + return None, errors
1.194 +
1.195 + periods.append(period)
1.196 +
1.197 + return periods, None
1.198 +
1.199 + def handle_period_controls(self, start_values, end_values, dtend_enabled, dttimes_enabled):
1.200 +
1.201 + """
1.202 + Handle datetime controls for a particular period, described by the given
1.203 + 'start_values' and 'end_values', with 'dtend_enabled' and
1.204 + 'dttimes_enabled' affecting the usage of the provided values.
1.205 + """
1.206 +
1.207 + t = self.handle_date_control_values(start_values, dttimes_enabled)
1.208 + if t:
1.209 + dtstart, dtstart_attr = t
1.210 + else:
1.211 + return None, ["dtstart"]
1.212 +
1.213 + # Handle specified end datetimes.
1.214 +
1.215 + if dtend_enabled:
1.216 + t = self.handle_date_control_values(end_values, dttimes_enabled)
1.217 + if t:
1.218 + dtend, dtend_attr = t
1.219 +
1.220 + # Convert end dates to iCalendar "next day" dates.
1.221 +
1.222 + if not isinstance(dtend, datetime):
1.223 + dtend += timedelta(1)
1.224 + else:
1.225 + return None, ["dtend"]
1.226 +
1.227 + # Otherwise, treat the end date as the start date. Datetimes are
1.228 + # handled by making the event occupy the rest of the day.
1.229 +
1.230 + else:
1.231 + dtend = dtstart + timedelta(1)
1.232 + dtend_attr = dtstart_attr
1.233 +
1.234 + if isinstance(dtstart, datetime):
1.235 + dtend = get_start_of_day(dtend, attr["TZID"])
1.236 +
1.237 + if dtstart >= dtend:
1.238 + return None, ["dtstart", "dtend"]
1.239 +
1.240 + return ((dtstart, dtstart_attr), (dtend, dtend_attr)), None
1.241 +
1.242 + def handle_date_control_values(self, values, with_time=True):
1.243 +
1.244 + """
1.245 + Handle date control information for the given 'values', returning a
1.246 + (datetime, attr) tuple, or None if the fields cannot be used to
1.247 + construct a datetime object.
1.248 + """
1.249 +
1.250 + if not values or not values["date"]:
1.251 + return None
1.252 + elif with_time:
1.253 + value = "%s%s" % (values["date"], values["time"])
1.254 + attr = {"TZID" : values["tzid"], "VALUE" : "DATE-TIME"}
1.255 + dt = get_datetime(value, attr)
1.256 + else:
1.257 + attr = {"VALUE" : "DATE"}
1.258 + dt = get_datetime(values["date"])
1.259 +
1.260 + if dt:
1.261 + return dt, attr
1.262 +
1.263 + return None
1.264 +
1.265 + def get_date_control_values(self, name, multiple=False):
1.266 +
1.267 + """
1.268 + Return a dictionary containing date, time and tzid entries for fields
1.269 + starting with 'name'.
1.270 + """
1.271 +
1.272 + args = self.env.get_args()
1.273 +
1.274 + dates = args.get("%s-date" % name, [])
1.275 + hours = args.get("%s-hour" % name, [])
1.276 + minutes = args.get("%s-minute" % name, [])
1.277 + seconds = args.get("%s-second" % name, [])
1.278 + tzids = args.get("%s-tzid" % name, [])
1.279 +
1.280 + # Handle absent values by employing None values.
1.281 +
1.282 + field_values = map(None, dates, hours, minutes, seconds, tzids)
1.283 + if not field_values and not multiple:
1.284 + field_values = [(None, None, None, None, None)]
1.285 +
1.286 + all_values = []
1.287 +
1.288 + for date, hour, minute, second, tzid in field_values:
1.289 +
1.290 + # Construct a usable dictionary of values.
1.291 +
1.292 + time = (hour or minute or second) and \
1.293 + "T%s%s%s" % (
1.294 + (hour or "").rjust(2, "0")[:2],
1.295 + (minute or "").rjust(2, "0")[:2],
1.296 + (second or "").rjust(2, "0")[:2]
1.297 + ) or ""
1.298 +
1.299 + value = {
1.300 + "date" : date,
1.301 + "time" : time,
1.302 + "tzid" : tzid or self.get_tzid()
1.303 + }
1.304 +
1.305 + # Return a single value or append to a collection of all values.
1.306 +
1.307 + if not multiple:
1.308 + return value
1.309 + else:
1.310 + all_values.append(value)
1.311 +
1.312 + return all_values
1.313 +
1.314 + def set_period_in_object(self, obj, period):
1.315 +
1.316 + "Set in the given 'obj' the given 'period' as the main start and end."
1.317 +
1.318 + (dtstart, dtstart_attr), (dtend, dtend_attr) = period
1.319 +
1.320 + return self.set_datetime_in_object(dtstart, dtstart_attr.get("TZID"), "DTSTART", obj) or \
1.321 + self.set_datetime_in_object(dtend, dtend_attr.get("TZID"), "DTEND", obj)
1.322 +
1.323 + def set_periods_in_object(self, obj, periods):
1.324 +
1.325 + "Set in the given 'obj' the given 'periods'."
1.326 +
1.327 + update = False
1.328 +
1.329 + old_values = obj.get_values("RDATE")
1.330 + new_rdates = []
1.331 +
1.332 + if obj.has_key("RDATE"):
1.333 + del obj["RDATE"]
1.334 +
1.335 + for period in periods:
1.336 + (dtstart, dtstart_attr), (dtend, dtend_attr) = period
1.337 + tzid = dtstart_attr.get("TZID") or dtend_attr.get("TZID")
1.338 + new_rdates.append(get_period_item(dtstart, dtend, tzid))
1.339 +
1.340 + obj["RDATE"] = new_rdates
1.341 +
1.342 + # NOTE: To do: calculate the update status.
1.343 + return update
1.344 +
1.345 + def set_datetime_in_object(self, dt, tzid, property, obj):
1.346 +
1.347 + """
1.348 + Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether
1.349 + an update has occurred.
1.350 + """
1.351 +
1.352 + if dt:
1.353 + old_value = obj.get_value(property)
1.354 + obj[property] = [get_datetime_item(dt, tzid)]
1.355 + return format_datetime(dt) != old_value
1.356 +
1.357 + return False
1.358 +
1.359 + def handle_new_attendees(self, obj):
1.360 +
1.361 + "Add or remove new attendees. This does not affect the stored object."
1.362 +
1.363 + args = self.env.get_args()
1.364 +
1.365 + existing_attendees = uri_values(obj.get_values("ATTENDEE") or [])
1.366 + new_attendees = args.get("added", [])
1.367 + new_attendee = args.get("attendee", [""])[0]
1.368 +
1.369 + if args.has_key("add"):
1.370 + if new_attendee.strip():
1.371 + new_attendee = get_uri(new_attendee.strip())
1.372 + if new_attendee not in new_attendees and new_attendee not in existing_attendees:
1.373 + new_attendees.append(new_attendee)
1.374 + new_attendee = ""
1.375 +
1.376 + if args.has_key("removenew"):
1.377 + removed_attendee = args["removenew"][0]
1.378 + if removed_attendee in new_attendees:
1.379 + new_attendees.remove(removed_attendee)
1.380 +
1.381 + return new_attendees, new_attendee
1.382 +
1.383 + def get_event_period(self, obj):
1.384 +
1.385 + """
1.386 + Return (dtstart, dtstart attributes), (dtend, dtend attributes) for
1.387 + 'obj'.
1.388 + """
1.389 +
1.390 + dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")
1.391 + if obj.has_key("DTEND"):
1.392 + dtend, dtend_attr = obj.get_datetime_item("DTEND")
1.393 + elif obj.has_key("DURATION"):
1.394 + duration = obj.get_duration("DURATION")
1.395 + dtend = dtstart + duration
1.396 + dtend_attr = dtstart_attr
1.397 + else:
1.398 + dtend, dtend_attr = dtstart, dtstart_attr
1.399 + return (dtstart, dtstart_attr), (dtend, dtend_attr)
1.400 +
1.401 + # Page fragment methods.
1.402 +
1.403 + def show_request_controls(self, obj):
1.404 +
1.405 + "Show form controls for a request concerning 'obj'."
1.406 +
1.407 + page = self.page
1.408 + args = self.env.get_args()
1.409 +
1.410 + is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.411 +
1.412 + attendees = uri_values((obj.get_values("ATTENDEE") or []) + filter(None, args.get("attendee", [])))
1.413 + is_attendee = self.user in attendees
1.414 +
1.415 + is_request = (obj.get_value("UID"), obj.get_value("RECURRENCE-ID")) in self._get_requests()
1.416 +
1.417 + have_other_attendees = len(attendees) > (is_attendee and 1 or 0)
1.418 +
1.419 + # Show appropriate options depending on the role of the user.
1.420 +
1.421 + if is_attendee and not is_organiser:
1.422 + page.p("An action is required for this request:")
1.423 +
1.424 + page.p()
1.425 + page.input(name="reply", type="submit", value="Send reply")
1.426 + page.add(" ")
1.427 + page.input(name="discard", type="submit", value="Discard event")
1.428 + page.add(" ")
1.429 + page.input(name="ignore", type="submit", value="Do nothing for now")
1.430 + page.p.close()
1.431 +
1.432 + if is_organiser:
1.433 + page.p("As organiser, you can perform the following:")
1.434 +
1.435 + if have_other_attendees:
1.436 + page.p()
1.437 + page.input(name="invite", type="submit", value="Invite/notify attendees")
1.438 + page.add(" ")
1.439 + if is_request:
1.440 + page.input(name="discard", type="submit", value="Discard event")
1.441 + else:
1.442 + page.input(name="cancel", type="submit", value="Cancel event")
1.443 + page.add(" ")
1.444 + page.input(name="ignore", type="submit", value="Do nothing for now")
1.445 + page.p.close()
1.446 + else:
1.447 + page.p()
1.448 + page.input(name="save", type="submit", value="Save event")
1.449 + page.add(" ")
1.450 + page.input(name="discard", type="submit", value="Discard event")
1.451 + page.add(" ")
1.452 + page.input(name="ignore", type="submit", value="Do nothing for now")
1.453 + page.p.close()
1.454 +
1.455 + property_items = [
1.456 + ("SUMMARY", "Summary"),
1.457 + ("DTSTART", "Start"),
1.458 + ("DTEND", "End"),
1.459 + ("ORGANIZER", "Organiser"),
1.460 + ("ATTENDEE", "Attendee"),
1.461 + ]
1.462 +
1.463 + partstat_items = [
1.464 + ("NEEDS-ACTION", "Not confirmed"),
1.465 + ("ACCEPTED", "Attending"),
1.466 + ("TENTATIVE", "Tentatively attending"),
1.467 + ("DECLINED", "Not attending"),
1.468 + ("DELEGATED", "Delegated"),
1.469 + (None, "Not indicated"),
1.470 + ]
1.471 +
1.472 + def show_object_on_page(self, uid, obj, error=None):
1.473 +
1.474 + """
1.475 + Show the calendar object with the given 'uid' and representation 'obj'
1.476 + on the current page. If 'error' is given, show a suitable message.
1.477 + """
1.478 +
1.479 + page = self.page
1.480 + page.form(method="POST")
1.481 +
1.482 + page.input(name="editing", type="hidden", value="true")
1.483 +
1.484 + args = self.env.get_args()
1.485 +
1.486 + # Obtain the user's timezone.
1.487 +
1.488 + tzid = self.get_tzid()
1.489 +
1.490 + # Obtain basic event information, showing any necessary editing controls.
1.491 +
1.492 + is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.493 +
1.494 + if is_organiser:
1.495 + new_attendees, new_attendee = self.handle_new_attendees(obj)
1.496 + else:
1.497 + new_attendees = []
1.498 + new_attendee = ""
1.499 +
1.500 + (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_event_period(obj)
1.501 + self.show_object_datetime_controls(dtstart, dtend)
1.502 +
1.503 + # Provide a summary of the object.
1.504 +
1.505 + page.table(class_="object", cellspacing=5, cellpadding=5)
1.506 + page.thead()
1.507 + page.tr()
1.508 + page.th("Event", class_="mainheading", colspan=2)
1.509 + page.tr.close()
1.510 + page.thead.close()
1.511 + page.tbody()
1.512 +
1.513 + for name, label in self.property_items:
1.514 + field = name.lower()
1.515 +
1.516 + items = obj.get_items(name) or []
1.517 + rowspan = len(items)
1.518 +
1.519 + if name == "ATTENDEE":
1.520 + rowspan += len(new_attendees) + 1
1.521 + elif not items:
1.522 + continue
1.523 +
1.524 + page.tr()
1.525 + page.th(label, class_="objectheading %s%s" % (field, error and field in error and " error" or ""), rowspan=rowspan)
1.526 +
1.527 + # Handle datetimes specially.
1.528 +
1.529 + if name in ["DTSTART", "DTEND"]:
1.530 +
1.531 + # Obtain the datetime.
1.532 +
1.533 + if name == "DTSTART":
1.534 + dt, attr = dtstart, dtstart_attr
1.535 +
1.536 + # Where no end datetime exists, use the start datetime as the
1.537 + # basis of any potential datetime specified if dt-control is
1.538 + # set.
1.539 +
1.540 + else:
1.541 + dt, attr = dtend or dtstart, dtend_attr or dtstart_attr
1.542 +
1.543 + self.show_datetime_controls(obj, dt, attr, name == "DTSTART")
1.544 +
1.545 + page.tr.close()
1.546 +
1.547 + # Handle the summary specially.
1.548 +
1.549 + elif name == "SUMMARY":
1.550 + value = args.get("summary", [obj.get_value(name)])[0]
1.551 +
1.552 + page.td()
1.553 + if is_organiser:
1.554 + page.input(name="summary", type="text", value=value, size=80)
1.555 + else:
1.556 + page.add(value)
1.557 + page.td.close()
1.558 + page.tr.close()
1.559 +
1.560 + # Handle potentially many values.
1.561 +
1.562 + else:
1.563 + first = True
1.564 +
1.565 + for i, (value, attr) in enumerate(items):
1.566 + if not first:
1.567 + page.tr()
1.568 + else:
1.569 + first = False
1.570 +
1.571 + if name == "ATTENDEE":
1.572 + value = get_uri(value)
1.573 +
1.574 + page.td(class_="objectvalue")
1.575 + page.add(value)
1.576 + page.add(" ")
1.577 +
1.578 + partstat = attr.get("PARTSTAT")
1.579 + if value == self.user:
1.580 + self._show_menu("partstat", partstat, self.partstat_items, "partstat")
1.581 + else:
1.582 + page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")
1.583 +
1.584 + if is_organiser:
1.585 + if value in args.get("remove", []):
1.586 + page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove", checked="checked")
1.587 + else:
1.588 + page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove")
1.589 + page.label("Remove", for_="remove-%d" % i, class_="remove")
1.590 + page.label("Uninvited", for_="remove-%d" % i, class_="removed")
1.591 +
1.592 + else:
1.593 + page.td(class_="objectvalue")
1.594 + page.add(value)
1.595 +
1.596 + page.td.close()
1.597 + page.tr.close()
1.598 +
1.599 + # Allow more attendees to be specified.
1.600 +
1.601 + if is_organiser and name == "ATTENDEE":
1.602 + for i, attendee in enumerate(new_attendees):
1.603 + if not first:
1.604 + page.tr()
1.605 + else:
1.606 + first = False
1.607 +
1.608 + page.td()
1.609 + page.input(name="added", type="value", value=attendee)
1.610 + page.input(name="removenew", type="submit", value=attendee, id="removenew-%d" % i, class_="remove")
1.611 + page.label("Remove", for_="removenew-%d" % i, class_="remove")
1.612 + page.td.close()
1.613 + page.tr.close()
1.614 +
1.615 + if not first:
1.616 + page.tr()
1.617 +
1.618 + page.td()
1.619 + page.input(name="attendee", type="value", value=new_attendee)
1.620 + page.input(name="add", type="submit", value="add", id="add-%d" % i, class_="add")
1.621 + page.label("Add", for_="add-%d" % i, class_="add")
1.622 + page.td.close()
1.623 + page.tr.close()
1.624 +
1.625 + page.tbody.close()
1.626 + page.table.close()
1.627 +
1.628 + self.show_recurrences(obj)
1.629 + self.show_conflicting_events(uid, obj)
1.630 + self.show_request_controls(obj)
1.631 +
1.632 + page.form.close()
1.633 +
1.634 + def show_object_datetime_controls(self, start, end, index=None):
1.635 +
1.636 + """
1.637 + Show datetime-related controls if already active or if an object needs
1.638 + them for the given 'start' to 'end' period. The given 'index' is used to
1.639 + parameterise individual controls for dynamic manipulation.
1.640 + """
1.641 +
1.642 + page = self.page
1.643 + args = self.env.get_args()
1.644 + sn = self._suffixed_name
1.645 + ssn = self._simple_suffixed_name
1.646 +
1.647 + # Add a dynamic stylesheet to permit the controls to modify the display.
1.648 + # NOTE: The style details need to be coordinated with the static
1.649 + # NOTE: stylesheet.
1.650 +
1.651 + if index is not None:
1.652 + page.style(type="text/css")
1.653 +
1.654 + # Unlike the rules for object properties, these affect recurrence
1.655 + # properties.
1.656 +
1.657 + page.add("""\
1.658 +input#dttimes-enable-%(index)d,
1.659 +input#dtend-enable-%(index)d,
1.660 +input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
1.661 +input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
1.662 +input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
1.663 +input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
1.664 + display: none;
1.665 +}""" % {"index" : index})
1.666 +
1.667 + page.style.close()
1.668 +
1.669 + dtend_control = args.get(ssn("dtend-control", "recur", index), [])
1.670 + dttimes_control = args.get(ssn("dttimes-control", "recur", index), [])
1.671 +
1.672 + dtend_enabled = index is not None and str(index) in dtend_control or index is None and dtend_control
1.673 + dttimes_enabled = index is not None and str(index) in dttimes_control or index is None and dttimes_control
1.674 +
1.675 + initial_load = not args.has_key("editing")
1.676 +
1.677 + dtend_enabled = dtend_enabled or initial_load and (isinstance(end, datetime) or start != end - timedelta(1))
1.678 + dttimes_enabled = dttimes_enabled or initial_load and (isinstance(start, datetime) or isinstance(end, datetime))
1.679 +
1.680 + if dtend_enabled:
1.681 + page.input(name=ssn("dtend-control", "recur", index), type="checkbox",
1.682 + value=(index is not None and str(index) or "enable"), id=sn("dtend-enable", index), checked="checked")
1.683 + else:
1.684 + page.input(name=ssn("dtend-control", "recur", index), type="checkbox",
1.685 + value=(index is not None and str(index) or "enable"), id=sn("dtend-enable", index))
1.686 +
1.687 + if dttimes_enabled:
1.688 + page.input(name=ssn("dttimes-control", "recur", index), type="checkbox",
1.689 + value=(index is not None and str(index) or "enable"), id=sn("dttimes-enable", index), checked="checked")
1.690 + else:
1.691 + page.input(name=ssn("dttimes-control", "recur", index), type="checkbox",
1.692 + value=(index is not None and str(index) or "enable"), id=sn("dttimes-enable", index))
1.693 +
1.694 + def show_datetime_controls(self, obj, dt, attr, show_start):
1.695 +
1.696 + """
1.697 + Show datetime details from the given 'obj' for the datetime 'dt' and
1.698 + attributes 'attr', showing start details if 'show_start' is set
1.699 + to a true value. Details will appear as controls for organisers and
1.700 + labels for attendees.
1.701 + """
1.702 +
1.703 + page = self.page
1.704 + is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.705 +
1.706 + # Change end dates to refer to the actual dates, not the iCalendar
1.707 + # "next day" dates.
1.708 +
1.709 + if not show_start and not isinstance(dt, datetime):
1.710 + dt -= timedelta(1)
1.711 +
1.712 + # Show controls for editing as organiser.
1.713 +
1.714 + if is_organiser:
1.715 + page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
1.716 +
1.717 + if show_start:
1.718 + page.div(class_="dt enabled")
1.719 + self._show_date_controls("dtstart", dt, attr.get("TZID"))
1.720 + page.br()
1.721 + page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
1.722 + page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
1.723 + page.div.close()
1.724 +
1.725 + else:
1.726 + page.div(class_="dt disabled")
1.727 + page.label("Specify end date", for_="dtend-enable", class_="enable")
1.728 + page.div.close()
1.729 + page.div(class_="dt enabled")
1.730 + self._show_date_controls("dtend", dt, attr.get("TZID"))
1.731 + page.br()
1.732 + page.label("End on same day", for_="dtend-enable", class_="disable")
1.733 + page.div.close()
1.734 +
1.735 + page.td.close()
1.736 +
1.737 + # Show a label as attendee.
1.738 +
1.739 + else:
1.740 + page.td(self.format_datetime(dt, "full"))
1.741 +
1.742 + def show_recurrence_controls(self, obj, index, start, end, origin, recurrenceid, recurrenceids, show_start):
1.743 +
1.744 + """
1.745 + Show datetime details from the given 'obj' for the recurrence having the
1.746 + given 'index', with the recurrence period described by the datetimes
1.747 + 'start' and 'end', indicating the 'origin' of the period from the event
1.748 + details, employing any 'recurrenceid' and 'recurrenceids' for the object
1.749 + to configure the displayed information.
1.750 +
1.751 + If 'show_start' is set to a true value, the start details will be shown;
1.752 + otherwise, the end details will be shown.
1.753 + """
1.754 +
1.755 + page = self.page
1.756 + sn = self._suffixed_name
1.757 + ssn = self._simple_suffixed_name
1.758 +
1.759 + is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.760 +
1.761 + # Change end dates to refer to the actual dates, not the iCalendar
1.762 + # "next day" dates.
1.763 +
1.764 + if not isinstance(end, datetime):
1.765 + end -= timedelta(1)
1.766 +
1.767 + start_utc = format_datetime(to_timezone(start, "UTC"))
1.768 + replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
1.769 + css = " ".join([
1.770 + replaced,
1.771 + recurrenceid and start_utc == recurrenceid and "affected" or ""
1.772 + ])
1.773 +
1.774 + # Show controls for editing as organiser.
1.775 +
1.776 + if is_organiser and not replaced and origin != "RRULE":
1.777 + page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
1.778 +
1.779 + if show_start:
1.780 + page.div(class_="dt enabled")
1.781 + self._show_date_controls(ssn("dtstart", "recur", index), start, None, index)
1.782 + page.br()
1.783 + page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable")
1.784 + page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable")
1.785 + page.div.close()
1.786 +
1.787 + else:
1.788 + page.div(class_="dt disabled")
1.789 + page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable")
1.790 + page.div.close()
1.791 + page.div(class_="dt enabled")
1.792 + self._show_date_controls(ssn("dtend", "recur", index), end, None, index)
1.793 + page.br()
1.794 + page.label("End on same day", for_=sn("dtend-enable", index), class_="disable")
1.795 + page.div.close()
1.796 +
1.797 + page.td.close()
1.798 +
1.799 + # Show label as attendee.
1.800 +
1.801 + else:
1.802 + page.td(self.format_datetime(show_start and start or end, "long"), class_=css)
1.803 +
1.804 + def show_recurrences(self, obj):
1.805 +
1.806 + "Show recurrences for the object having the given representation 'obj'."
1.807 +
1.808 + page = self.page
1.809 + is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.810 +
1.811 + # Obtain any parent object if this object is a specific recurrence.
1.812 +
1.813 + uid = obj.get_value("UID")
1.814 + recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
1.815 +
1.816 + if recurrenceid:
1.817 + obj = self._get_object(uid)
1.818 + if not obj:
1.819 + return
1.820 +
1.821 + page.p("This event modifies a recurring event.")
1.822 +
1.823 + # Obtain the periods associated with the event in the user's time zone.
1.824 +
1.825 + periods = obj.get_periods(self.get_tzid(), self.get_window_end(), origin=True)
1.826 + recurrenceids = self._get_recurrences(uid)
1.827 +
1.828 + if len(periods) == 1:
1.829 + return
1.830 +
1.831 + if is_organiser:
1.832 + page.p("This event recurs on the following occasions within the next %d days:" % self.get_window_size())
1.833 + else:
1.834 + page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
1.835 +
1.836 + # Determine whether any periods are explicitly created or are part of a
1.837 + # rule.
1.838 +
1.839 + explicit_periods = filter(lambda t: t[2] != "RRULE", periods)
1.840 +
1.841 + # Show each recurrence in a separate table if editable.
1.842 +
1.843 + if is_organiser and explicit_periods:
1.844 +
1.845 + for index, (start, end, origin) in enumerate(periods[1:]):
1.846 +
1.847 + # Isolate the controls from neighbouring tables.
1.848 +
1.849 + page.div()
1.850 +
1.851 + self.show_object_datetime_controls(start, end, index)
1.852 +
1.853 + # NOTE: Need to customise the TH classes according to errors and
1.854 + # NOTE: index information.
1.855 +
1.856 + page.table(cellspacing=5, cellpadding=5, class_="recurrence")
1.857 + page.caption("Occurrence")
1.858 + page.tbody()
1.859 + page.tr()
1.860 + page.th("Start", class_="objectheading start")
1.861 + self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True)
1.862 + page.tr.close()
1.863 + page.tr()
1.864 + page.th("End", class_="objectheading end")
1.865 + self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False)
1.866 + page.tr.close()
1.867 + page.tbody.close()
1.868 + page.table.close()
1.869 +
1.870 + page.div.close()
1.871 +
1.872 + # Otherwise, use a compact single table.
1.873 +
1.874 + else:
1.875 + page.table(cellspacing=5, cellpadding=5, class_="recurrence")
1.876 + page.caption("Occurrences")
1.877 + page.thead()
1.878 + page.tr()
1.879 + page.th("Start", class_="objectheading start")
1.880 + page.th("End", class_="objectheading end")
1.881 + page.tr.close()
1.882 + page.thead.close()
1.883 + page.tbody()
1.884 +
1.885 + # Show only subsequent periods if organiser, since the principal
1.886 + # period will be the start and end datetimes.
1.887 +
1.888 + for index, (start, end, origin) in enumerate(is_organiser and periods[1:] or periods):
1.889 + page.tr()
1.890 + self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True)
1.891 + self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False)
1.892 + page.tr.close()
1.893 + page.tbody.close()
1.894 + page.table.close()
1.895 +
1.896 + def show_conflicting_events(self, uid, obj):
1.897 +
1.898 + """
1.899 + Show conflicting events for the object having the given 'uid' and
1.900 + representation 'obj'.
1.901 + """
1.902 +
1.903 + page = self.page
1.904 +
1.905 + # Obtain the user's timezone.
1.906 +
1.907 + tzid = self.get_tzid()
1.908 + periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())
1.909 +
1.910 + # Indicate whether there are conflicting events.
1.911 +
1.912 + freebusy = self.store.get_freebusy(self.user)
1.913 +
1.914 + if freebusy:
1.915 +
1.916 + # Obtain any time zone details from the suggested event.
1.917 +
1.918 + _dtstart, attr = obj.get_item("DTSTART")
1.919 + tzid = attr.get("TZID", tzid)
1.920 +
1.921 + # Show any conflicts.
1.922 +
1.923 + conflicts = [t for t in have_conflict(freebusy, periods, True) if t[2] != uid]
1.924 +
1.925 + if conflicts:
1.926 + page.p("This event conflicts with others:")
1.927 +
1.928 + page.table(cellspacing=5, cellpadding=5, class_="conflicts")
1.929 + page.thead()
1.930 + page.tr()
1.931 + page.th("Event")
1.932 + page.th("Start")
1.933 + page.th("End")
1.934 + page.tr.close()
1.935 + page.thead.close()
1.936 + page.tbody()
1.937 +
1.938 + for t in conflicts:
1.939 + start, end, found_uid, transp, found_recurrenceid, summary = t[:6]
1.940 +
1.941 + # Provide details of any conflicting event.
1.942 +
1.943 + start = self.format_datetime(to_timezone(get_datetime(start), tzid), "long")
1.944 + end = self.format_datetime(to_timezone(get_datetime(end), tzid), "long")
1.945 +
1.946 + page.tr()
1.947 +
1.948 + # Show the event summary for the conflicting event.
1.949 +
1.950 + page.td()
1.951 + page.a(summary, href=self.link_to(found_uid))
1.952 + page.td.close()
1.953 +
1.954 + page.td(start)
1.955 + page.td(end)
1.956 +
1.957 + page.tr.close()
1.958 +
1.959 + page.tbody.close()
1.960 + page.table.close()
1.961 +
1.962 + # Full page output methods.
1.963 +
1.964 + def show(self, path_info):
1.965 +
1.966 + "Show an object request using the given 'path_info' for the current user."
1.967 +
1.968 + uid, recurrenceid = self._get_identifiers(path_info)
1.969 + obj = self._get_object(uid, recurrenceid)
1.970 +
1.971 + if not obj:
1.972 + return False
1.973 +
1.974 + error = self.handle_request(uid, recurrenceid, obj)
1.975 +
1.976 + if not error:
1.977 + return True
1.978 +
1.979 + self.new_page(title="Event")
1.980 + self.show_object_on_page(uid, obj, error)
1.981 +
1.982 + return True
1.983 +
1.984 + # Utility methods.
1.985 +
1.986 + def _show_menu(self, name, default, items, class_="", index=None):
1.987 +
1.988 + """
1.989 + Show a select menu having the given 'name', set to the given 'default',
1.990 + providing the given (value, label) 'items', and employing the given CSS
1.991 + 'class_' if specified.
1.992 + """
1.993 +
1.994 + page = self.page
1.995 + values = self.env.get_args().get(name, [default])
1.996 + if index is not None:
1.997 + values = values[index:]
1.998 + values = values and values[0:1] or [default]
1.999 +
1.1000 + page.select(name=name, class_=class_)
1.1001 + for v, label in items:
1.1002 + if v is None:
1.1003 + continue
1.1004 + if v in values:
1.1005 + page.option(label, value=v, selected="selected")
1.1006 + else:
1.1007 + page.option(label, value=v)
1.1008 + page.select.close()
1.1009 +
1.1010 + def _show_date_controls(self, name, default, tzid, index=None):
1.1011 +
1.1012 + """
1.1013 + Show date controls for a field with the given 'name' and 'default' value
1.1014 + and 'tzid'.
1.1015 + """
1.1016 +
1.1017 + page = self.page
1.1018 + args = self.env.get_args()
1.1019 +
1.1020 + event_tzid = tzid or self.get_tzid()
1.1021 +
1.1022 + # Show dates for up to one week around the current date.
1.1023 +
1.1024 + base = to_date(default)
1.1025 + items = []
1.1026 + for i in range(-7, 8):
1.1027 + d = base + timedelta(i)
1.1028 + items.append((format_datetime(d), self.format_date(d, "full")))
1.1029 +
1.1030 + self._show_menu("%s-date" % name, format_datetime(base), items, index=index)
1.1031 +
1.1032 + # Show time details.
1.1033 +
1.1034 + default_time = isinstance(default, datetime) and default or None
1.1035 +
1.1036 + hour = args.get("%s-hour" % name, [])[index or 0:]
1.1037 + hour = hour and hour[0] or "%02d" % (default_time and default_time.hour or 0)
1.1038 + minute = args.get("%s-minute" % name, [])[index or 0:]
1.1039 + minute = minute and minute[0] or "%02d" % (default_time and default_time.minute or 0)
1.1040 + second = args.get("%s-second" % name, [])[index or 0:]
1.1041 + second = second and second[0] or "%02d" % (default_time and default_time.second or 0)
1.1042 +
1.1043 + page.span(class_="time enabled")
1.1044 + page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2)
1.1045 + page.add(":")
1.1046 + page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2)
1.1047 + page.add(":")
1.1048 + page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2)
1.1049 + page.add(" ")
1.1050 + self._show_timezone_menu("%s-tzid" % name, event_tzid, index)
1.1051 + page.span.close()
1.1052 +
1.1053 + def _show_timezone_menu(self, name, default, index=None):
1.1054 +
1.1055 + """
1.1056 + Show timezone controls using a menu with the given 'name', set to the
1.1057 + given 'default' unless a field of the given 'name' provides a value.
1.1058 + """
1.1059 +
1.1060 + entries = [(tzid, tzid) for tzid in pytz.all_timezones]
1.1061 + self._show_menu(name, default, entries, index=index)
1.1062 +
1.1063 +# vim: tabstop=4 expandtab shiftwidth=4