1.1 --- a/imip_manager.py Thu Mar 26 00:27:06 2015 +0100
1.2 +++ b/imip_manager.py Thu Mar 26 16:11:46 2015 +0100
1.3 @@ -24,1907 +24,17 @@
1.4
1.5 LIBRARY_PATH = "/var/lib/imip-agent"
1.6
1.7 -from datetime import date, datetime, timedelta
1.8 -import babel.dates
1.9 -import pytz
1.10 import sys
1.11 -
1.12 sys.path.append(LIBRARY_PATH)
1.13
1.14 -from imiptools.client import Client, update_attendees
1.15 -from imiptools.data import get_address, get_uri, get_window_end, Object, \
1.16 - uri_dict, uri_values
1.17 -from imiptools.dates import format_datetime, format_time, to_date, get_datetime, \
1.18 - get_datetime_item, get_end_of_day, get_period_item, \
1.19 - get_start_of_day, get_start_of_next_day, get_timestamp, \
1.20 - ends_on_same_day, to_timezone
1.21 -from imiptools.mail import Messenger
1.22 -from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
1.23 - convert_periods, get_freebusy_details, \
1.24 - get_scale, have_conflict, get_slots, get_spans, \
1.25 - partition_by_day, remove_period, remove_affected_period, \
1.26 - update_freebusy
1.27 -from imipweb.env import CGIEnvironment
1.28 -from imipweb.handler import ManagerHandler
1.29 -import imip_store
1.30 -import markup
1.31 +from imipweb.calendar import CalendarPage
1.32 +from imipweb.event import EventPage
1.33 +from imipweb.resource import Resource
1.34
1.35 -class Manager(Client):
1.36 +class Manager(Resource):
1.37
1.38 "A simple manager application."
1.39
1.40 - def __init__(self, messenger=None):
1.41 - self.messenger = messenger or Messenger()
1.42 - self.encoding = "utf-8"
1.43 - self.env = CGIEnvironment(self.encoding)
1.44 -
1.45 - user = self.env.get_user()
1.46 - Client.__init__(self, user and get_uri(user) or None)
1.47 -
1.48 - self.locale = None
1.49 - self.requests = None
1.50 -
1.51 - self.out = self.env.get_output()
1.52 - self.page = markup.page()
1.53 - self.html_ids = None
1.54 -
1.55 - self.store = imip_store.FileStore()
1.56 - self.objects = {}
1.57 -
1.58 - try:
1.59 - self.publisher = imip_store.FilePublisher()
1.60 - except OSError:
1.61 - self.publisher = None
1.62 -
1.63 - def _suffixed_name(self, name, index=None):
1.64 - return index is not None and "%s-%d" % (name, index) or name
1.65 -
1.66 - def _simple_suffixed_name(self, name, suffix, index=None):
1.67 - return index is not None and "%s-%s" % (name, suffix) or name
1.68 -
1.69 - def _get_identifiers(self, path_info):
1.70 - parts = path_info.lstrip("/").split("/")
1.71 - if len(parts) == 1:
1.72 - return parts[0], None
1.73 - else:
1.74 - return parts[:2]
1.75 -
1.76 - def _get_object(self, uid, recurrenceid=None):
1.77 - if self.objects.has_key((uid, recurrenceid)):
1.78 - return self.objects[(uid, recurrenceid)]
1.79 -
1.80 - fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None
1.81 - obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment)
1.82 - return obj
1.83 -
1.84 - def _get_recurrences(self, uid):
1.85 - return self.store.get_recurrences(self.user, uid)
1.86 -
1.87 - def _get_requests(self):
1.88 - if self.requests is None:
1.89 - cancellations = self.store.get_cancellations(self.user)
1.90 - requests = set(self.store.get_requests(self.user))
1.91 - self.requests = requests.difference(cancellations)
1.92 - return self.requests
1.93 -
1.94 - def _get_request_summary(self):
1.95 - summary = []
1.96 - for uid, recurrenceid in self._get_requests():
1.97 - obj = self._get_object(uid, recurrenceid)
1.98 - if obj:
1.99 - periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())
1.100 - recurrenceids = self._get_recurrences(uid)
1.101 -
1.102 - # Convert the periods to more substantial free/busy items.
1.103 -
1.104 - for start, end in periods:
1.105 -
1.106 - # Subtract any recurrences from the free/busy details of a
1.107 - # parent object.
1.108 -
1.109 - if recurrenceid or start not in recurrenceids:
1.110 - summary.append((
1.111 - start, end, uid,
1.112 - obj.get_value("TRANSP"),
1.113 - recurrenceid,
1.114 - obj.get_value("SUMMARY"),
1.115 - obj.get_value("ORGANIZER")
1.116 - ))
1.117 - return summary
1.118 -
1.119 - # Preference methods.
1.120 -
1.121 - def get_user_locale(self):
1.122 - if not self.locale:
1.123 - self.locale = self.get_preferences().get("LANG", "en")
1.124 - return self.locale
1.125 -
1.126 - # Prettyprinting of dates and times.
1.127 -
1.128 - def format_date(self, dt, format):
1.129 - return self._format_datetime(babel.dates.format_date, dt, format)
1.130 -
1.131 - def format_time(self, dt, format):
1.132 - return self._format_datetime(babel.dates.format_time, dt, format)
1.133 -
1.134 - def format_datetime(self, dt, format):
1.135 - return self._format_datetime(
1.136 - isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,
1.137 - dt, format)
1.138 -
1.139 - def _format_datetime(self, fn, dt, format):
1.140 - return fn(dt, format=format, locale=self.get_user_locale())
1.141 -
1.142 - # Data management methods.
1.143 -
1.144 - def remove_request(self, uid, recurrenceid=None):
1.145 - return self.store.dequeue_request(self.user, uid, recurrenceid)
1.146 -
1.147 - def remove_event(self, uid, recurrenceid=None):
1.148 - return self.store.remove_event(self.user, uid, recurrenceid)
1.149 -
1.150 - def update_freebusy(self, uid, recurrenceid, obj):
1.151 -
1.152 - """
1.153 - Update stored free/busy details for the event with the given 'uid' and
1.154 - 'recurrenceid' having a representation of 'obj'.
1.155 - """
1.156 -
1.157 - is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE"))
1.158 -
1.159 - freebusy = self.store.get_freebusy(self.user)
1.160 -
1.161 - update_freebusy(freebusy,
1.162 - obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()),
1.163 - is_only_organiser and "ORG" or obj.get_value("TRANSP"),
1.164 - uid, recurrenceid,
1.165 - obj.get_value("SUMMARY"),
1.166 - obj.get_value("ORGANIZER"))
1.167 -
1.168 - # Subtract any recurrences from the free/busy details of a parent
1.169 - # object.
1.170 -
1.171 - for recurrenceid in self._get_recurrences(uid):
1.172 - remove_affected_period(freebusy, uid, recurrenceid)
1.173 -
1.174 - self.store.set_freebusy(self.user, freebusy)
1.175 -
1.176 - def remove_from_freebusy(self, uid, recurrenceid=None):
1.177 - freebusy = self.store.get_freebusy(self.user)
1.178 - remove_period(freebusy, uid, recurrenceid)
1.179 - self.store.set_freebusy(self.user, freebusy)
1.180 -
1.181 - # Presentation methods.
1.182 -
1.183 - def new_page(self, title):
1.184 - self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))
1.185 - self.html_ids = set()
1.186 -
1.187 - def status(self, code, message):
1.188 - self.header("Status", "%s %s" % (code, message))
1.189 -
1.190 - def header(self, header, value):
1.191 - print >>self.out, "%s: %s" % (header, value)
1.192 -
1.193 - def no_user(self):
1.194 - self.status(403, "Forbidden")
1.195 - self.new_page(title="Forbidden")
1.196 - self.page.p("You are not logged in and thus cannot access scheduling requests.")
1.197 -
1.198 - def no_page(self):
1.199 - self.status(404, "Not Found")
1.200 - self.new_page(title="Not Found")
1.201 - self.page.p("No page is provided at the given address.")
1.202 -
1.203 - def redirect(self, url):
1.204 - self.status(302, "Redirect")
1.205 - self.header("Location", url)
1.206 - self.new_page(title="Redirect")
1.207 - self.page.p("Redirecting to: %s" % url)
1.208 -
1.209 - def link_to(self, uid, recurrenceid=None):
1.210 - if recurrenceid:
1.211 - return self.env.new_url("/".join([uid, recurrenceid]))
1.212 - else:
1.213 - return self.env.new_url(uid)
1.214 -
1.215 - # Request logic methods.
1.216 -
1.217 - def handle_newevent(self):
1.218 -
1.219 - """
1.220 - Handle any new event operation, creating a new event and redirecting to
1.221 - the event page for further activity.
1.222 - """
1.223 -
1.224 - # Handle a submitted form.
1.225 -
1.226 - args = self.env.get_args()
1.227 -
1.228 - if not args.has_key("newevent"):
1.229 - return
1.230 -
1.231 - # Create a new event using the available information.
1.232 -
1.233 - slots = args.get("slot", [])
1.234 - participants = args.get("participants", [])
1.235 -
1.236 - if not slots:
1.237 - return
1.238 -
1.239 - # Obtain the user's timezone.
1.240 -
1.241 - tzid = self.get_tzid()
1.242 -
1.243 - # Coalesce the selected slots.
1.244 -
1.245 - slots.sort()
1.246 - coalesced = []
1.247 - last = None
1.248 -
1.249 - for slot in slots:
1.250 - start, end = slot.split("-")
1.251 - start = get_datetime(start, {"TZID" : tzid})
1.252 - end = end and get_datetime(end, {"TZID" : tzid}) or get_start_of_next_day(start, tzid)
1.253 -
1.254 - if last:
1.255 - last_start, last_end = last
1.256 -
1.257 - # Merge adjacent dates and datetimes.
1.258 -
1.259 - if start == last_end or \
1.260 - not isinstance(start, datetime) and \
1.261 - get_start_of_day(last_end, tzid) == get_start_of_day(start, tzid):
1.262 -
1.263 - last = last_start, end
1.264 - continue
1.265 -
1.266 - # Handle datetimes within dates.
1.267 - # Datetime periods are within single days and are therefore
1.268 - # discarded.
1.269 -
1.270 - elif not isinstance(last_start, datetime) and \
1.271 - get_start_of_day(start, tzid) == get_start_of_day(last_start, tzid):
1.272 -
1.273 - continue
1.274 -
1.275 - # Add separate dates and datetimes.
1.276 -
1.277 - else:
1.278 - coalesced.append(last)
1.279 -
1.280 - last = start, end
1.281 -
1.282 - if last:
1.283 - coalesced.append(last)
1.284 -
1.285 - # Invent a unique identifier.
1.286 -
1.287 - utcnow = get_timestamp()
1.288 - uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
1.289 -
1.290 - # Create a calendar object and store it as a request.
1.291 -
1.292 - record = []
1.293 - rwrite = record.append
1.294 -
1.295 - # Define a single occurrence if only one coalesced slot exists.
1.296 -
1.297 - start, end = coalesced[0]
1.298 - start_value, start_attr = get_datetime_item(start, tzid)
1.299 - end_value, end_attr = get_datetime_item(end, tzid)
1.300 -
1.301 - rwrite(("UID", {}, uid))
1.302 - rwrite(("SUMMARY", {}, "New event at %s" % utcnow))
1.303 - rwrite(("DTSTAMP", {}, utcnow))
1.304 - rwrite(("DTSTART", start_attr, start_value))
1.305 - rwrite(("DTEND", end_attr, end_value))
1.306 - rwrite(("ORGANIZER", {}, self.user))
1.307 -
1.308 - participants = uri_values(filter(None, participants))
1.309 -
1.310 - for participant in participants:
1.311 - rwrite(("ATTENDEE", {"RSVP" : "TRUE", "PARTSTAT" : "NEEDS-ACTION"}, participant))
1.312 -
1.313 - if self.user not in participants:
1.314 - rwrite(("ATTENDEE", {"PARTSTAT" : "ACCEPTED"}, self.user))
1.315 -
1.316 - # Define additional occurrences if many slots are defined.
1.317 -
1.318 - rdates = []
1.319 -
1.320 - for start, end in coalesced[1:]:
1.321 - start_value, start_attr = get_datetime_item(start, tzid)
1.322 - end_value, end_attr = get_datetime_item(end, tzid)
1.323 - rdates.append("%s/%s" % (start_value, end_value))
1.324 -
1.325 - if rdates:
1.326 - rwrite(("RDATE", {"VALUE" : "PERIOD", "TZID" : tzid}, rdates))
1.327 -
1.328 - node = ("VEVENT", {}, record)
1.329 -
1.330 - self.store.set_event(self.user, uid, None, node=node)
1.331 - self.store.queue_request(self.user, uid)
1.332 -
1.333 - # Redirect to the object (or the first of the objects), where instead of
1.334 - # attendee controls, there will be organiser controls.
1.335 -
1.336 - self.redirect(self.link_to(uid))
1.337 -
1.338 - def handle_request(self, uid, recurrenceid, obj):
1.339 -
1.340 - """
1.341 - Handle actions involving the given 'uid', 'recurrenceid', and 'obj' as
1.342 - the object's representation, returning an error if one occurred, or None
1.343 - if the request was successfully handled.
1.344 - """
1.345 -
1.346 - # Handle a submitted form.
1.347 -
1.348 - args = self.env.get_args()
1.349 -
1.350 - # Get the possible actions.
1.351 -
1.352 - reply = args.has_key("reply")
1.353 - discard = args.has_key("discard")
1.354 - invite = args.has_key("invite")
1.355 - cancel = args.has_key("cancel")
1.356 - save = args.has_key("save")
1.357 - ignore = args.has_key("ignore")
1.358 -
1.359 - have_action = reply or discard or invite or cancel or save or ignore
1.360 -
1.361 - if not have_action:
1.362 - return ["action"]
1.363 -
1.364 - # If ignoring the object, return to the calendar.
1.365 -
1.366 - if ignore:
1.367 - self.redirect(self.env.get_path())
1.368 - return None
1.369 -
1.370 - # Update the object.
1.371 -
1.372 - if args.has_key("summary"):
1.373 - obj["SUMMARY"] = [(args["summary"][0], {})]
1.374 -
1.375 - attendees = uri_dict(obj.get_value_map("ATTENDEE"))
1.376 -
1.377 - if args.has_key("partstat"):
1.378 - if attendees.has_key(self.user):
1.379 - attendees[self.user]["PARTSTAT"] = args["partstat"][0]
1.380 - if attendees[self.user].has_key("RSVP"):
1.381 - del attendees[self.user]["RSVP"]
1.382 -
1.383 - is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.384 -
1.385 - # Obtain the user's timezone and process datetime values.
1.386 -
1.387 - update = False
1.388 -
1.389 - if is_organiser:
1.390 - periods, errors = self.handle_all_period_controls()
1.391 - if errors:
1.392 - return errors
1.393 - elif periods:
1.394 - self.set_period_in_object(obj, periods[0])
1.395 - self.set_periods_in_object(obj, periods[1:])
1.396 -
1.397 - # Obtain any participants to be added or removed.
1.398 -
1.399 - removed = args.get("remove")
1.400 - added = args.get("added")
1.401 -
1.402 - # Process any action.
1.403 -
1.404 - handled = True
1.405 -
1.406 - if reply or invite or cancel:
1.407 -
1.408 - handler = ManagerHandler(obj, self.user, self.messenger)
1.409 -
1.410 - # Process the object and remove it from the list of requests.
1.411 -
1.412 - if reply and handler.process_received_request(update) or \
1.413 - is_organiser and (invite or cancel) and \
1.414 - handler.process_created_request(invite and "REQUEST" or "CANCEL", update, removed, added):
1.415 -
1.416 - self.remove_request(uid, recurrenceid)
1.417 -
1.418 - # Save single user events.
1.419 -
1.420 - elif save:
1.421 - to_cancel = update_attendees(obj, added, removed)
1.422 - self.store.set_event(self.user, uid, recurrenceid, node=obj.to_node())
1.423 - self.update_freebusy(uid, recurrenceid, obj)
1.424 - self.remove_request(uid, recurrenceid)
1.425 -
1.426 - # Remove the request and the object.
1.427 -
1.428 - elif discard:
1.429 - self.remove_from_freebusy(uid, recurrenceid)
1.430 - self.remove_event(uid, recurrenceid)
1.431 - self.remove_request(uid, recurrenceid)
1.432 -
1.433 - else:
1.434 - handled = False
1.435 -
1.436 - # Upon handling an action, redirect to the main page.
1.437 -
1.438 - if handled:
1.439 - self.redirect(self.env.get_path())
1.440 -
1.441 - return None
1.442 -
1.443 - def handle_all_period_controls(self):
1.444 -
1.445 - """
1.446 - Handle datetime controls for a particular period, where 'index' may be
1.447 - used to indicate a recurring period, or the main start and end datetimes
1.448 - are handled.
1.449 - """
1.450 -
1.451 - args = self.env.get_args()
1.452 -
1.453 - periods = []
1.454 -
1.455 - # Get the main period details.
1.456 -
1.457 - dtend_enabled = args.get("dtend-control", [None])[0]
1.458 - dttimes_enabled = args.get("dttimes-control", [None])[0]
1.459 - start_values = self.get_date_control_values("dtstart")
1.460 - end_values = self.get_date_control_values("dtend")
1.461 -
1.462 - period, errors = self.handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled)
1.463 -
1.464 - if errors:
1.465 - return None, errors
1.466 -
1.467 - periods.append(period)
1.468 -
1.469 - # Get the recurring period details.
1.470 -
1.471 - all_dtend_enabled = args.get("dtend-control-recur", [])
1.472 - all_dttimes_enabled = args.get("dttimes-control-recur", [])
1.473 - all_start_values = self.get_date_control_values("dtstart-recur", multiple=True)
1.474 - all_end_values = self.get_date_control_values("dtend-recur", multiple=True)
1.475 -
1.476 - for index, (start_values, end_values, dtend_enabled, dttimes_enabled) in \
1.477 - enumerate(map(None, all_start_values, all_end_values, all_dtend_enabled, all_dttimes_enabled)):
1.478 -
1.479 - dtend_enabled = str(index) in all_dtend_enabled
1.480 - dttimes_enabled = str(index) in all_dttimes_enabled
1.481 - period, errors = self.handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled)
1.482 -
1.483 - if errors:
1.484 - return None, errors
1.485 -
1.486 - periods.append(period)
1.487 -
1.488 - return periods, None
1.489 -
1.490 - def handle_period_controls(self, start_values, end_values, dtend_enabled, dttimes_enabled):
1.491 -
1.492 - """
1.493 - Handle datetime controls for a particular period, described by the given
1.494 - 'start_values' and 'end_values', with 'dtend_enabled' and
1.495 - 'dttimes_enabled' affecting the usage of the provided values.
1.496 - """
1.497 -
1.498 - t = self.handle_date_control_values(start_values, dttimes_enabled)
1.499 - if t:
1.500 - dtstart, dtstart_attr = t
1.501 - else:
1.502 - return None, ["dtstart"]
1.503 -
1.504 - # Handle specified end datetimes.
1.505 -
1.506 - if dtend_enabled:
1.507 - t = self.handle_date_control_values(end_values, dttimes_enabled)
1.508 - if t:
1.509 - dtend, dtend_attr = t
1.510 -
1.511 - # Convert end dates to iCalendar "next day" dates.
1.512 -
1.513 - if not isinstance(dtend, datetime):
1.514 - dtend += timedelta(1)
1.515 - else:
1.516 - return None, ["dtend"]
1.517 -
1.518 - # Otherwise, treat the end date as the start date. Datetimes are
1.519 - # handled by making the event occupy the rest of the day.
1.520 -
1.521 - else:
1.522 - dtend = dtstart + timedelta(1)
1.523 - dtend_attr = dtstart_attr
1.524 -
1.525 - if isinstance(dtstart, datetime):
1.526 - dtend = get_start_of_day(dtend, attr["TZID"])
1.527 -
1.528 - if dtstart >= dtend:
1.529 - return None, ["dtstart", "dtend"]
1.530 -
1.531 - return ((dtstart, dtstart_attr), (dtend, dtend_attr)), None
1.532 -
1.533 - def handle_date_control_values(self, values, with_time=True):
1.534 -
1.535 - """
1.536 - Handle date control information for the given 'values', returning a
1.537 - (datetime, attr) tuple, or None if the fields cannot be used to
1.538 - construct a datetime object.
1.539 - """
1.540 -
1.541 - if not values or not values["date"]:
1.542 - return None
1.543 - elif with_time:
1.544 - value = "%s%s" % (values["date"], values["time"])
1.545 - attr = {"TZID" : values["tzid"], "VALUE" : "DATE-TIME"}
1.546 - dt = get_datetime(value, attr)
1.547 - else:
1.548 - attr = {"VALUE" : "DATE"}
1.549 - dt = get_datetime(values["date"])
1.550 -
1.551 - if dt:
1.552 - return dt, attr
1.553 -
1.554 - return None
1.555 -
1.556 - def get_date_control_values(self, name, multiple=False):
1.557 -
1.558 - """
1.559 - Return a dictionary containing date, time and tzid entries for fields
1.560 - starting with 'name'.
1.561 - """
1.562 -
1.563 - args = self.env.get_args()
1.564 -
1.565 - dates = args.get("%s-date" % name, [])
1.566 - hours = args.get("%s-hour" % name, [])
1.567 - minutes = args.get("%s-minute" % name, [])
1.568 - seconds = args.get("%s-second" % name, [])
1.569 - tzids = args.get("%s-tzid" % name, [])
1.570 -
1.571 - # Handle absent values by employing None values.
1.572 -
1.573 - field_values = map(None, dates, hours, minutes, seconds, tzids)
1.574 - if not field_values and not multiple:
1.575 - field_values = [(None, None, None, None, None)]
1.576 -
1.577 - all_values = []
1.578 -
1.579 - for date, hour, minute, second, tzid in field_values:
1.580 -
1.581 - # Construct a usable dictionary of values.
1.582 -
1.583 - time = (hour or minute or second) and \
1.584 - "T%s%s%s" % (
1.585 - (hour or "").rjust(2, "0")[:2],
1.586 - (minute or "").rjust(2, "0")[:2],
1.587 - (second or "").rjust(2, "0")[:2]
1.588 - ) or ""
1.589 -
1.590 - value = {
1.591 - "date" : date,
1.592 - "time" : time,
1.593 - "tzid" : tzid or self.get_tzid()
1.594 - }
1.595 -
1.596 - # Return a single value or append to a collection of all values.
1.597 -
1.598 - if not multiple:
1.599 - return value
1.600 - else:
1.601 - all_values.append(value)
1.602 -
1.603 - return all_values
1.604 -
1.605 - def set_period_in_object(self, obj, period):
1.606 -
1.607 - "Set in the given 'obj' the given 'period' as the main start and end."
1.608 -
1.609 - (dtstart, dtstart_attr), (dtend, dtend_attr) = period
1.610 -
1.611 - return self.set_datetime_in_object(dtstart, dtstart_attr.get("TZID"), "DTSTART", obj) or \
1.612 - self.set_datetime_in_object(dtend, dtend_attr.get("TZID"), "DTEND", obj)
1.613 -
1.614 - def set_periods_in_object(self, obj, periods):
1.615 -
1.616 - "Set in the given 'obj' the given 'periods'."
1.617 -
1.618 - update = False
1.619 -
1.620 - old_values = obj.get_values("RDATE")
1.621 - new_rdates = []
1.622 -
1.623 - if obj.has_key("RDATE"):
1.624 - del obj["RDATE"]
1.625 -
1.626 - for period in periods:
1.627 - (dtstart, dtstart_attr), (dtend, dtend_attr) = period
1.628 - tzid = dtstart_attr.get("TZID") or dtend_attr.get("TZID")
1.629 - new_rdates.append(get_period_item(dtstart, dtend, tzid))
1.630 -
1.631 - obj["RDATE"] = new_rdates
1.632 -
1.633 - # NOTE: To do: calculate the update status.
1.634 - return update
1.635 -
1.636 - def set_datetime_in_object(self, dt, tzid, property, obj):
1.637 -
1.638 - """
1.639 - Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether
1.640 - an update has occurred.
1.641 - """
1.642 -
1.643 - if dt:
1.644 - old_value = obj.get_value(property)
1.645 - obj[property] = [get_datetime_item(dt, tzid)]
1.646 - return format_datetime(dt) != old_value
1.647 -
1.648 - return False
1.649 -
1.650 - def handle_new_attendees(self, obj):
1.651 -
1.652 - "Add or remove new attendees. This does not affect the stored object."
1.653 -
1.654 - args = self.env.get_args()
1.655 -
1.656 - existing_attendees = uri_values(obj.get_values("ATTENDEE") or [])
1.657 - new_attendees = args.get("added", [])
1.658 - new_attendee = args.get("attendee", [""])[0]
1.659 -
1.660 - if args.has_key("add"):
1.661 - if new_attendee.strip():
1.662 - new_attendee = get_uri(new_attendee.strip())
1.663 - if new_attendee not in new_attendees and new_attendee not in existing_attendees:
1.664 - new_attendees.append(new_attendee)
1.665 - new_attendee = ""
1.666 -
1.667 - if args.has_key("removenew"):
1.668 - removed_attendee = args["removenew"][0]
1.669 - if removed_attendee in new_attendees:
1.670 - new_attendees.remove(removed_attendee)
1.671 -
1.672 - return new_attendees, new_attendee
1.673 -
1.674 - def get_event_period(self, obj):
1.675 -
1.676 - """
1.677 - Return (dtstart, dtstart attributes), (dtend, dtend attributes) for
1.678 - 'obj'.
1.679 - """
1.680 -
1.681 - dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")
1.682 - if obj.has_key("DTEND"):
1.683 - dtend, dtend_attr = obj.get_datetime_item("DTEND")
1.684 - elif obj.has_key("DURATION"):
1.685 - duration = obj.get_duration("DURATION")
1.686 - dtend = dtstart + duration
1.687 - dtend_attr = dtstart_attr
1.688 - else:
1.689 - dtend, dtend_attr = dtstart, dtstart_attr
1.690 - return (dtstart, dtstart_attr), (dtend, dtend_attr)
1.691 -
1.692 - # Page fragment methods.
1.693 -
1.694 - def show_request_controls(self, obj):
1.695 -
1.696 - "Show form controls for a request concerning 'obj'."
1.697 -
1.698 - page = self.page
1.699 - args = self.env.get_args()
1.700 -
1.701 - is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.702 -
1.703 - attendees = uri_values((obj.get_values("ATTENDEE") or []) + filter(None, args.get("attendee", [])))
1.704 - is_attendee = self.user in attendees
1.705 -
1.706 - is_request = (obj.get_value("UID"), obj.get_value("RECURRENCE-ID")) in self._get_requests()
1.707 -
1.708 - have_other_attendees = len(attendees) > (is_attendee and 1 or 0)
1.709 -
1.710 - # Show appropriate options depending on the role of the user.
1.711 -
1.712 - if is_attendee and not is_organiser:
1.713 - page.p("An action is required for this request:")
1.714 -
1.715 - page.p()
1.716 - page.input(name="reply", type="submit", value="Send reply")
1.717 - page.add(" ")
1.718 - page.input(name="discard", type="submit", value="Discard event")
1.719 - page.add(" ")
1.720 - page.input(name="ignore", type="submit", value="Do nothing for now")
1.721 - page.p.close()
1.722 -
1.723 - if is_organiser:
1.724 - page.p("As organiser, you can perform the following:")
1.725 -
1.726 - if have_other_attendees:
1.727 - page.p()
1.728 - page.input(name="invite", type="submit", value="Invite/notify attendees")
1.729 - page.add(" ")
1.730 - if is_request:
1.731 - page.input(name="discard", type="submit", value="Discard event")
1.732 - else:
1.733 - page.input(name="cancel", type="submit", value="Cancel event")
1.734 - page.add(" ")
1.735 - page.input(name="ignore", type="submit", value="Do nothing for now")
1.736 - page.p.close()
1.737 - else:
1.738 - page.p()
1.739 - page.input(name="save", type="submit", value="Save event")
1.740 - page.add(" ")
1.741 - page.input(name="discard", type="submit", value="Discard event")
1.742 - page.add(" ")
1.743 - page.input(name="ignore", type="submit", value="Do nothing for now")
1.744 - page.p.close()
1.745 -
1.746 - property_items = [
1.747 - ("SUMMARY", "Summary"),
1.748 - ("DTSTART", "Start"),
1.749 - ("DTEND", "End"),
1.750 - ("ORGANIZER", "Organiser"),
1.751 - ("ATTENDEE", "Attendee"),
1.752 - ]
1.753 -
1.754 - partstat_items = [
1.755 - ("NEEDS-ACTION", "Not confirmed"),
1.756 - ("ACCEPTED", "Attending"),
1.757 - ("TENTATIVE", "Tentatively attending"),
1.758 - ("DECLINED", "Not attending"),
1.759 - ("DELEGATED", "Delegated"),
1.760 - (None, "Not indicated"),
1.761 - ]
1.762 -
1.763 - def show_object_on_page(self, uid, obj, error=None):
1.764 -
1.765 - """
1.766 - Show the calendar object with the given 'uid' and representation 'obj'
1.767 - on the current page. If 'error' is given, show a suitable message.
1.768 - """
1.769 -
1.770 - page = self.page
1.771 - page.form(method="POST")
1.772 -
1.773 - page.input(name="editing", type="hidden", value="true")
1.774 -
1.775 - args = self.env.get_args()
1.776 -
1.777 - # Obtain the user's timezone.
1.778 -
1.779 - tzid = self.get_tzid()
1.780 -
1.781 - # Obtain basic event information, showing any necessary editing controls.
1.782 -
1.783 - is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.784 -
1.785 - if is_organiser:
1.786 - new_attendees, new_attendee = self.handle_new_attendees(obj)
1.787 - else:
1.788 - new_attendees = []
1.789 - new_attendee = ""
1.790 -
1.791 - (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_event_period(obj)
1.792 - self.show_object_datetime_controls(dtstart, dtend)
1.793 -
1.794 - # Provide a summary of the object.
1.795 -
1.796 - page.table(class_="object", cellspacing=5, cellpadding=5)
1.797 - page.thead()
1.798 - page.tr()
1.799 - page.th("Event", class_="mainheading", colspan=2)
1.800 - page.tr.close()
1.801 - page.thead.close()
1.802 - page.tbody()
1.803 -
1.804 - for name, label in self.property_items:
1.805 - field = name.lower()
1.806 -
1.807 - items = obj.get_items(name) or []
1.808 - rowspan = len(items)
1.809 -
1.810 - if name == "ATTENDEE":
1.811 - rowspan += len(new_attendees) + 1
1.812 - elif not items:
1.813 - continue
1.814 -
1.815 - page.tr()
1.816 - page.th(label, class_="objectheading %s%s" % (field, error and field in error and " error" or ""), rowspan=rowspan)
1.817 -
1.818 - # Handle datetimes specially.
1.819 -
1.820 - if name in ["DTSTART", "DTEND"]:
1.821 -
1.822 - # Obtain the datetime.
1.823 -
1.824 - if name == "DTSTART":
1.825 - dt, attr = dtstart, dtstart_attr
1.826 -
1.827 - # Where no end datetime exists, use the start datetime as the
1.828 - # basis of any potential datetime specified if dt-control is
1.829 - # set.
1.830 -
1.831 - else:
1.832 - dt, attr = dtend or dtstart, dtend_attr or dtstart_attr
1.833 -
1.834 - self.show_datetime_controls(obj, dt, attr, name == "DTSTART")
1.835 -
1.836 - page.tr.close()
1.837 -
1.838 - # Handle the summary specially.
1.839 -
1.840 - elif name == "SUMMARY":
1.841 - value = args.get("summary", [obj.get_value(name)])[0]
1.842 -
1.843 - page.td()
1.844 - if is_organiser:
1.845 - page.input(name="summary", type="text", value=value, size=80)
1.846 - else:
1.847 - page.add(value)
1.848 - page.td.close()
1.849 - page.tr.close()
1.850 -
1.851 - # Handle potentially many values.
1.852 -
1.853 - else:
1.854 - first = True
1.855 -
1.856 - for i, (value, attr) in enumerate(items):
1.857 - if not first:
1.858 - page.tr()
1.859 - else:
1.860 - first = False
1.861 -
1.862 - if name == "ATTENDEE":
1.863 - value = get_uri(value)
1.864 -
1.865 - page.td(class_="objectvalue")
1.866 - page.add(value)
1.867 - page.add(" ")
1.868 -
1.869 - partstat = attr.get("PARTSTAT")
1.870 - if value == self.user:
1.871 - self._show_menu("partstat", partstat, self.partstat_items, "partstat")
1.872 - else:
1.873 - page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")
1.874 -
1.875 - if is_organiser:
1.876 - if value in args.get("remove", []):
1.877 - page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove", checked="checked")
1.878 - else:
1.879 - page.input(name="remove", type="checkbox", value=value, id="remove-%d" % i, class_="remove")
1.880 - page.label("Remove", for_="remove-%d" % i, class_="remove")
1.881 - page.label("Uninvited", for_="remove-%d" % i, class_="removed")
1.882 -
1.883 - else:
1.884 - page.td(class_="objectvalue")
1.885 - page.add(value)
1.886 -
1.887 - page.td.close()
1.888 - page.tr.close()
1.889 -
1.890 - # Allow more attendees to be specified.
1.891 -
1.892 - if is_organiser and name == "ATTENDEE":
1.893 - for i, attendee in enumerate(new_attendees):
1.894 - if not first:
1.895 - page.tr()
1.896 - else:
1.897 - first = False
1.898 -
1.899 - page.td()
1.900 - page.input(name="added", type="value", value=attendee)
1.901 - page.input(name="removenew", type="submit", value=attendee, id="removenew-%d" % i, class_="remove")
1.902 - page.label("Remove", for_="removenew-%d" % i, class_="remove")
1.903 - page.td.close()
1.904 - page.tr.close()
1.905 -
1.906 - if not first:
1.907 - page.tr()
1.908 -
1.909 - page.td()
1.910 - page.input(name="attendee", type="value", value=new_attendee)
1.911 - page.input(name="add", type="submit", value="add", id="add-%d" % i, class_="add")
1.912 - page.label("Add", for_="add-%d" % i, class_="add")
1.913 - page.td.close()
1.914 - page.tr.close()
1.915 -
1.916 - page.tbody.close()
1.917 - page.table.close()
1.918 -
1.919 - self.show_recurrences(obj)
1.920 - self.show_conflicting_events(uid, obj)
1.921 - self.show_request_controls(obj)
1.922 -
1.923 - page.form.close()
1.924 -
1.925 - def show_object_datetime_controls(self, start, end, index=None):
1.926 -
1.927 - """
1.928 - Show datetime-related controls if already active or if an object needs
1.929 - them for the given 'start' to 'end' period. The given 'index' is used to
1.930 - parameterise individual controls for dynamic manipulation.
1.931 - """
1.932 -
1.933 - page = self.page
1.934 - args = self.env.get_args()
1.935 - sn = self._suffixed_name
1.936 - ssn = self._simple_suffixed_name
1.937 -
1.938 - # Add a dynamic stylesheet to permit the controls to modify the display.
1.939 - # NOTE: The style details need to be coordinated with the static
1.940 - # NOTE: stylesheet.
1.941 -
1.942 - if index is not None:
1.943 - page.style(type="text/css")
1.944 -
1.945 - # Unlike the rules for object properties, these affect recurrence
1.946 - # properties.
1.947 -
1.948 - page.add("""\
1.949 -input#dttimes-enable-%(index)d,
1.950 -input#dtend-enable-%(index)d,
1.951 -input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
1.952 -input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
1.953 -input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
1.954 -input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
1.955 - display: none;
1.956 -}""" % {"index" : index})
1.957 -
1.958 - page.style.close()
1.959 -
1.960 - dtend_control = args.get(ssn("dtend-control", "recur", index), [])
1.961 - dttimes_control = args.get(ssn("dttimes-control", "recur", index), [])
1.962 -
1.963 - dtend_enabled = index is not None and str(index) in dtend_control or index is None and dtend_control
1.964 - dttimes_enabled = index is not None and str(index) in dttimes_control or index is None and dttimes_control
1.965 -
1.966 - initial_load = not args.has_key("editing")
1.967 -
1.968 - dtend_enabled = dtend_enabled or initial_load and (isinstance(end, datetime) or start != end - timedelta(1))
1.969 - dttimes_enabled = dttimes_enabled or initial_load and (isinstance(start, datetime) or isinstance(end, datetime))
1.970 -
1.971 - if dtend_enabled:
1.972 - page.input(name=ssn("dtend-control", "recur", index), type="checkbox",
1.973 - value=(index is not None and str(index) or "enable"), id=sn("dtend-enable", index), checked="checked")
1.974 - else:
1.975 - page.input(name=ssn("dtend-control", "recur", index), type="checkbox",
1.976 - value=(index is not None and str(index) or "enable"), id=sn("dtend-enable", index))
1.977 -
1.978 - if dttimes_enabled:
1.979 - page.input(name=ssn("dttimes-control", "recur", index), type="checkbox",
1.980 - value=(index is not None and str(index) or "enable"), id=sn("dttimes-enable", index), checked="checked")
1.981 - else:
1.982 - page.input(name=ssn("dttimes-control", "recur", index), type="checkbox",
1.983 - value=(index is not None and str(index) or "enable"), id=sn("dttimes-enable", index))
1.984 -
1.985 - def show_datetime_controls(self, obj, dt, attr, show_start):
1.986 -
1.987 - """
1.988 - Show datetime details from the given 'obj' for the datetime 'dt' and
1.989 - attributes 'attr', showing start details if 'show_start' is set
1.990 - to a true value. Details will appear as controls for organisers and
1.991 - labels for attendees.
1.992 - """
1.993 -
1.994 - page = self.page
1.995 - is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.996 -
1.997 - # Change end dates to refer to the actual dates, not the iCalendar
1.998 - # "next day" dates.
1.999 -
1.1000 - if not show_start and not isinstance(dt, datetime):
1.1001 - dt -= timedelta(1)
1.1002 -
1.1003 - # Show controls for editing as organiser.
1.1004 -
1.1005 - if is_organiser:
1.1006 - page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
1.1007 -
1.1008 - if show_start:
1.1009 - page.div(class_="dt enabled")
1.1010 - self._show_date_controls("dtstart", dt, attr.get("TZID"))
1.1011 - page.br()
1.1012 - page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
1.1013 - page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
1.1014 - page.div.close()
1.1015 -
1.1016 - else:
1.1017 - page.div(class_="dt disabled")
1.1018 - page.label("Specify end date", for_="dtend-enable", class_="enable")
1.1019 - page.div.close()
1.1020 - page.div(class_="dt enabled")
1.1021 - self._show_date_controls("dtend", dt, attr.get("TZID"))
1.1022 - page.br()
1.1023 - page.label("End on same day", for_="dtend-enable", class_="disable")
1.1024 - page.div.close()
1.1025 -
1.1026 - page.td.close()
1.1027 -
1.1028 - # Show a label as attendee.
1.1029 -
1.1030 - else:
1.1031 - page.td(self.format_datetime(dt, "full"))
1.1032 -
1.1033 - def show_recurrence_controls(self, obj, index, start, end, origin, recurrenceid, recurrenceids, show_start):
1.1034 -
1.1035 - """
1.1036 - Show datetime details from the given 'obj' for the recurrence having the
1.1037 - given 'index', with the recurrence period described by the datetimes
1.1038 - 'start' and 'end', indicating the 'origin' of the period from the event
1.1039 - details, employing any 'recurrenceid' and 'recurrenceids' for the object
1.1040 - to configure the displayed information.
1.1041 -
1.1042 - If 'show_start' is set to a true value, the start details will be shown;
1.1043 - otherwise, the end details will be shown.
1.1044 - """
1.1045 -
1.1046 - page = self.page
1.1047 - sn = self._suffixed_name
1.1048 - ssn = self._simple_suffixed_name
1.1049 -
1.1050 - is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.1051 -
1.1052 - # Change end dates to refer to the actual dates, not the iCalendar
1.1053 - # "next day" dates.
1.1054 -
1.1055 - if not isinstance(end, datetime):
1.1056 - end -= timedelta(1)
1.1057 -
1.1058 - start_utc = format_datetime(to_timezone(start, "UTC"))
1.1059 - replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
1.1060 - css = " ".join([
1.1061 - replaced,
1.1062 - recurrenceid and start_utc == recurrenceid and "affected" or ""
1.1063 - ])
1.1064 -
1.1065 - # Show controls for editing as organiser.
1.1066 -
1.1067 - if is_organiser and not replaced and origin != "RRULE":
1.1068 - page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
1.1069 -
1.1070 - if show_start:
1.1071 - page.div(class_="dt enabled")
1.1072 - self._show_date_controls(ssn("dtstart", "recur", index), start, None, index)
1.1073 - page.br()
1.1074 - page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable")
1.1075 - page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable")
1.1076 - page.div.close()
1.1077 -
1.1078 - else:
1.1079 - page.div(class_="dt disabled")
1.1080 - page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable")
1.1081 - page.div.close()
1.1082 - page.div(class_="dt enabled")
1.1083 - self._show_date_controls(ssn("dtend", "recur", index), end, None, index)
1.1084 - page.br()
1.1085 - page.label("End on same day", for_=sn("dtend-enable", index), class_="disable")
1.1086 - page.div.close()
1.1087 -
1.1088 - page.td.close()
1.1089 -
1.1090 - # Show label as attendee.
1.1091 -
1.1092 - else:
1.1093 - page.td(self.format_datetime(show_start and start or end, "long"), class_=css)
1.1094 -
1.1095 - def show_recurrences(self, obj):
1.1096 -
1.1097 - "Show recurrences for the object having the given representation 'obj'."
1.1098 -
1.1099 - page = self.page
1.1100 - is_organiser = get_uri(obj.get_value("ORGANIZER")) == self.user
1.1101 -
1.1102 - # Obtain any parent object if this object is a specific recurrence.
1.1103 -
1.1104 - uid = obj.get_value("UID")
1.1105 - recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
1.1106 -
1.1107 - if recurrenceid:
1.1108 - obj = self._get_object(uid)
1.1109 - if not obj:
1.1110 - return
1.1111 -
1.1112 - page.p("This event modifies a recurring event.")
1.1113 -
1.1114 - # Obtain the periods associated with the event in the user's time zone.
1.1115 -
1.1116 - periods = obj.get_periods(self.get_tzid(), self.get_window_end(), origin=True)
1.1117 - recurrenceids = self._get_recurrences(uid)
1.1118 -
1.1119 - if len(periods) == 1:
1.1120 - return
1.1121 -
1.1122 - if is_organiser:
1.1123 - page.p("This event recurs on the following occasions within the next %d days:" % self.get_window_size())
1.1124 - else:
1.1125 - page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
1.1126 -
1.1127 - # Determine whether any periods are explicitly created or are part of a
1.1128 - # rule.
1.1129 -
1.1130 - explicit_periods = filter(lambda t: t[2] != "RRULE", periods)
1.1131 -
1.1132 - # Show each recurrence in a separate table if editable.
1.1133 -
1.1134 - if is_organiser and explicit_periods:
1.1135 -
1.1136 - for index, (start, end, origin) in enumerate(periods[1:]):
1.1137 -
1.1138 - # Isolate the controls from neighbouring tables.
1.1139 -
1.1140 - page.div()
1.1141 -
1.1142 - self.show_object_datetime_controls(start, end, index)
1.1143 -
1.1144 - # NOTE: Need to customise the TH classes according to errors and
1.1145 - # NOTE: index information.
1.1146 -
1.1147 - page.table(cellspacing=5, cellpadding=5, class_="recurrence")
1.1148 - page.caption("Occurrence")
1.1149 - page.tbody()
1.1150 - page.tr()
1.1151 - page.th("Start", class_="objectheading start")
1.1152 - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True)
1.1153 - page.tr.close()
1.1154 - page.tr()
1.1155 - page.th("End", class_="objectheading end")
1.1156 - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False)
1.1157 - page.tr.close()
1.1158 - page.tbody.close()
1.1159 - page.table.close()
1.1160 -
1.1161 - page.div.close()
1.1162 -
1.1163 - # Otherwise, use a compact single table.
1.1164 -
1.1165 - else:
1.1166 - page.table(cellspacing=5, cellpadding=5, class_="recurrence")
1.1167 - page.caption("Occurrences")
1.1168 - page.thead()
1.1169 - page.tr()
1.1170 - page.th("Start", class_="objectheading start")
1.1171 - page.th("End", class_="objectheading end")
1.1172 - page.tr.close()
1.1173 - page.thead.close()
1.1174 - page.tbody()
1.1175 -
1.1176 - # Show only subsequent periods if organiser, since the principal
1.1177 - # period will be the start and end datetimes.
1.1178 -
1.1179 - for index, (start, end, origin) in enumerate(is_organiser and periods[1:] or periods):
1.1180 - page.tr()
1.1181 - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, True)
1.1182 - self.show_recurrence_controls(obj, index, start, end, origin, recurrenceid, recurrenceids, False)
1.1183 - page.tr.close()
1.1184 - page.tbody.close()
1.1185 - page.table.close()
1.1186 -
1.1187 - def show_conflicting_events(self, uid, obj):
1.1188 -
1.1189 - """
1.1190 - Show conflicting events for the object having the given 'uid' and
1.1191 - representation 'obj'.
1.1192 - """
1.1193 -
1.1194 - page = self.page
1.1195 -
1.1196 - # Obtain the user's timezone.
1.1197 -
1.1198 - tzid = self.get_tzid()
1.1199 - periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())
1.1200 -
1.1201 - # Indicate whether there are conflicting events.
1.1202 -
1.1203 - freebusy = self.store.get_freebusy(self.user)
1.1204 -
1.1205 - if freebusy:
1.1206 -
1.1207 - # Obtain any time zone details from the suggested event.
1.1208 -
1.1209 - _dtstart, attr = obj.get_item("DTSTART")
1.1210 - tzid = attr.get("TZID", tzid)
1.1211 -
1.1212 - # Show any conflicts.
1.1213 -
1.1214 - conflicts = [t for t in have_conflict(freebusy, periods, True) if t[2] != uid]
1.1215 -
1.1216 - if conflicts:
1.1217 - page.p("This event conflicts with others:")
1.1218 -
1.1219 - page.table(cellspacing=5, cellpadding=5, class_="conflicts")
1.1220 - page.thead()
1.1221 - page.tr()
1.1222 - page.th("Event")
1.1223 - page.th("Start")
1.1224 - page.th("End")
1.1225 - page.tr.close()
1.1226 - page.thead.close()
1.1227 - page.tbody()
1.1228 -
1.1229 - for t in conflicts:
1.1230 - start, end, found_uid, transp, found_recurrenceid, summary = t[:6]
1.1231 -
1.1232 - # Provide details of any conflicting event.
1.1233 -
1.1234 - start = self.format_datetime(to_timezone(get_datetime(start), tzid), "long")
1.1235 - end = self.format_datetime(to_timezone(get_datetime(end), tzid), "long")
1.1236 -
1.1237 - page.tr()
1.1238 -
1.1239 - # Show the event summary for the conflicting event.
1.1240 -
1.1241 - page.td()
1.1242 - page.a(summary, href=self.link_to(found_uid))
1.1243 - page.td.close()
1.1244 -
1.1245 - page.td(start)
1.1246 - page.td(end)
1.1247 -
1.1248 - page.tr.close()
1.1249 -
1.1250 - page.tbody.close()
1.1251 - page.table.close()
1.1252 -
1.1253 - def show_requests_on_page(self):
1.1254 -
1.1255 - "Show requests for the current user."
1.1256 -
1.1257 - page = self.page
1.1258 -
1.1259 - # NOTE: This list could be more informative, but it is envisaged that
1.1260 - # NOTE: the requests would be visited directly anyway.
1.1261 -
1.1262 - requests = self._get_requests()
1.1263 -
1.1264 - page.div(id="pending-requests")
1.1265 -
1.1266 - if requests:
1.1267 - page.p("Pending requests:")
1.1268 -
1.1269 - page.ul()
1.1270 -
1.1271 - for uid, recurrenceid in requests:
1.1272 - obj = self._get_object(uid, recurrenceid)
1.1273 - if obj:
1.1274 - page.li()
1.1275 - page.a(obj.get_value("SUMMARY"), href="#request-%s-%s" % (uid, recurrenceid or ""))
1.1276 - page.li.close()
1.1277 -
1.1278 - page.ul.close()
1.1279 -
1.1280 - else:
1.1281 - page.p("There are no pending requests.")
1.1282 -
1.1283 - page.div.close()
1.1284 -
1.1285 - def show_participants_on_page(self):
1.1286 -
1.1287 - "Show participants for scheduling purposes."
1.1288 -
1.1289 - page = self.page
1.1290 - args = self.env.get_args()
1.1291 - participants = args.get("participants", [])
1.1292 -
1.1293 - try:
1.1294 - for name, value in args.items():
1.1295 - if name.startswith("remove-participant-"):
1.1296 - i = int(name[len("remove-participant-"):])
1.1297 - del participants[i]
1.1298 - break
1.1299 - except ValueError:
1.1300 - pass
1.1301 -
1.1302 - # Trim empty participants.
1.1303 -
1.1304 - while participants and not participants[-1].strip():
1.1305 - participants.pop()
1.1306 -
1.1307 - # Show any specified participants together with controls to remove and
1.1308 - # add participants.
1.1309 -
1.1310 - page.div(id="participants")
1.1311 -
1.1312 - page.p("Participants for scheduling:")
1.1313 -
1.1314 - for i, participant in enumerate(participants):
1.1315 - page.p()
1.1316 - page.input(name="participants", type="text", value=participant)
1.1317 - page.input(name="remove-participant-%d" % i, type="submit", value="Remove")
1.1318 - page.p.close()
1.1319 -
1.1320 - page.p()
1.1321 - page.input(name="participants", type="text")
1.1322 - page.input(name="add-participant", type="submit", value="Add")
1.1323 - page.p.close()
1.1324 -
1.1325 - page.div.close()
1.1326 -
1.1327 - return participants
1.1328 -
1.1329 - # Full page output methods.
1.1330 -
1.1331 - def show_object(self, path_info):
1.1332 -
1.1333 - "Show an object request using the given 'path_info' for the current user."
1.1334 -
1.1335 - uid, recurrenceid = self._get_identifiers(path_info)
1.1336 - obj = self._get_object(uid, recurrenceid)
1.1337 -
1.1338 - if not obj:
1.1339 - return False
1.1340 -
1.1341 - error = self.handle_request(uid, recurrenceid, obj)
1.1342 -
1.1343 - if not error:
1.1344 - return True
1.1345 -
1.1346 - self.new_page(title="Event")
1.1347 - self.show_object_on_page(uid, obj, error)
1.1348 -
1.1349 - return True
1.1350 -
1.1351 - def show_calendar(self):
1.1352 -
1.1353 - "Show the calendar for the current user."
1.1354 -
1.1355 - handled = self.handle_newevent()
1.1356 -
1.1357 - self.new_page(title="Calendar")
1.1358 - page = self.page
1.1359 -
1.1360 - # Form controls are used in various places on the calendar page.
1.1361 -
1.1362 - page.form(method="POST")
1.1363 -
1.1364 - self.show_requests_on_page()
1.1365 - participants = self.show_participants_on_page()
1.1366 -
1.1367 - # Show a button for scheduling a new event.
1.1368 -
1.1369 - page.p(class_="controls")
1.1370 - page.input(name="newevent", type="submit", value="New event", id="newevent", accesskey="N")
1.1371 - page.p.close()
1.1372 -
1.1373 - # Show controls for hiding empty days and busy slots.
1.1374 - # The positioning of the control, paragraph and table are important here.
1.1375 -
1.1376 - page.input(name="showdays", type="checkbox", value="show", id="showdays", accesskey="D")
1.1377 - page.input(name="hidebusy", type="checkbox", value="hide", id="hidebusy", accesskey="B")
1.1378 -
1.1379 - page.p(class_="controls")
1.1380 - page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable")
1.1381 - page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable")
1.1382 - page.label("Show empty days", for_="showdays", class_="showdays disable")
1.1383 - page.label("Hide empty days", for_="showdays", class_="showdays enable")
1.1384 - page.input(name="reset", type="submit", value="Clear selections", id="reset")
1.1385 - page.label("Clear selections", for_="reset", class_="reset")
1.1386 - page.p.close()
1.1387 -
1.1388 - freebusy = self.store.get_freebusy(self.user)
1.1389 -
1.1390 - if not freebusy:
1.1391 - page.p("No events scheduled.")
1.1392 - return
1.1393 -
1.1394 - # Obtain the user's timezone.
1.1395 -
1.1396 - tzid = self.get_tzid()
1.1397 -
1.1398 - # Day view: start at the earliest known day and produce days until the
1.1399 - # latest known day, perhaps with expandable sections of empty days.
1.1400 -
1.1401 - # Month view: start at the earliest known month and produce months until
1.1402 - # the latest known month, perhaps with expandable sections of empty
1.1403 - # months.
1.1404 -
1.1405 - # Details of users to invite to new events could be superimposed on the
1.1406 - # calendar.
1.1407 -
1.1408 - # Requests are listed and linked to their tentative positions in the
1.1409 - # calendar. Other participants are also shown.
1.1410 -
1.1411 - request_summary = self._get_request_summary()
1.1412 -
1.1413 - period_groups = [request_summary, freebusy]
1.1414 - period_group_types = ["request", "freebusy"]
1.1415 - period_group_sources = ["Pending requests", "Your schedule"]
1.1416 -
1.1417 - for i, participant in enumerate(participants):
1.1418 - period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant)))
1.1419 - period_group_types.append("freebusy-part%d" % i)
1.1420 - period_group_sources.append(participant)
1.1421 -
1.1422 - groups = []
1.1423 - group_columns = []
1.1424 - group_types = period_group_types
1.1425 - group_sources = period_group_sources
1.1426 - all_points = set()
1.1427 -
1.1428 - # Obtain time point information for each group of periods.
1.1429 -
1.1430 - for periods in period_groups:
1.1431 - periods = convert_periods(periods, tzid)
1.1432 -
1.1433 - # Get the time scale with start and end points.
1.1434 -
1.1435 - scale = get_scale(periods)
1.1436 -
1.1437 - # Get the time slots for the periods.
1.1438 -
1.1439 - slots = get_slots(scale)
1.1440 -
1.1441 - # Add start of day time points for multi-day periods.
1.1442 -
1.1443 - add_day_start_points(slots, tzid)
1.1444 -
1.1445 - # Record the slots and all time points employed.
1.1446 -
1.1447 - groups.append(slots)
1.1448 - all_points.update([point for point, active in slots])
1.1449 -
1.1450 - # Partition the groups into days.
1.1451 -
1.1452 - days = {}
1.1453 - partitioned_groups = []
1.1454 - partitioned_group_types = []
1.1455 - partitioned_group_sources = []
1.1456 -
1.1457 - for slots, group_type, group_source in zip(groups, group_types, group_sources):
1.1458 -
1.1459 - # Propagate time points to all groups of time slots.
1.1460 -
1.1461 - add_slots(slots, all_points)
1.1462 -
1.1463 - # Count the number of columns employed by the group.
1.1464 -
1.1465 - columns = 0
1.1466 -
1.1467 - # Partition the time slots by day.
1.1468 -
1.1469 - partitioned = {}
1.1470 -
1.1471 - for day, day_slots in partition_by_day(slots).items():
1.1472 -
1.1473 - # Construct a list of time intervals within the day.
1.1474 -
1.1475 - intervals = []
1.1476 - last = None
1.1477 -
1.1478 - for point, active in day_slots:
1.1479 - columns = max(columns, len(active))
1.1480 - if last:
1.1481 - intervals.append((last, point))
1.1482 - last = point
1.1483 -
1.1484 - if last:
1.1485 - intervals.append((last, None))
1.1486 -
1.1487 - if not days.has_key(day):
1.1488 - days[day] = set()
1.1489 -
1.1490 - # Convert each partition to a mapping from points to active
1.1491 - # periods.
1.1492 -
1.1493 - partitioned[day] = dict(day_slots)
1.1494 -
1.1495 - # Record the divisions or intervals within each day.
1.1496 -
1.1497 - days[day].update(intervals)
1.1498 -
1.1499 - # Only include the requests column if it provides objects.
1.1500 -
1.1501 - if group_type != "request" or columns:
1.1502 - group_columns.append(columns)
1.1503 - partitioned_groups.append(partitioned)
1.1504 - partitioned_group_types.append(group_type)
1.1505 - partitioned_group_sources.append(group_source)
1.1506 -
1.1507 - # Add empty days.
1.1508 -
1.1509 - add_empty_days(days, tzid)
1.1510 -
1.1511 - # Show the controls permitting day selection.
1.1512 -
1.1513 - self.show_calendar_day_controls(days)
1.1514 -
1.1515 - # Show the calendar itself.
1.1516 -
1.1517 - page.table(cellspacing=5, cellpadding=5, class_="calendar")
1.1518 - self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)
1.1519 - self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns)
1.1520 - page.table.close()
1.1521 -
1.1522 - # End the form region.
1.1523 -
1.1524 - page.form.close()
1.1525 -
1.1526 - # More page fragment methods.
1.1527 -
1.1528 - def show_calendar_day_controls(self, days):
1.1529 -
1.1530 - "Show controls for the given 'days' in the calendar."
1.1531 -
1.1532 - page = self.page
1.1533 - slots = self.env.get_args().get("slot", [])
1.1534 -
1.1535 - for day in days:
1.1536 - value, identifier = self._day_value_and_identifier(day)
1.1537 - self._slot_selector(value, identifier, slots)
1.1538 -
1.1539 - # Generate a dynamic stylesheet to allow day selections to colour
1.1540 - # specific days.
1.1541 - # NOTE: The style details need to be coordinated with the static
1.1542 - # NOTE: stylesheet.
1.1543 -
1.1544 - page.style(type="text/css")
1.1545 -
1.1546 - for day in days:
1.1547 - daystr = format_datetime(day)
1.1548 - page.add("""\
1.1549 -input.newevent.selector#day-%s-:checked ~ table label.day.day-%s,
1.1550 -input.newevent.selector#day-%s-:checked ~ table label.timepoint.day-%s {
1.1551 - background-color: #5f4;
1.1552 - text-decoration: underline;
1.1553 -}
1.1554 -""" % (daystr, daystr, daystr, daystr))
1.1555 -
1.1556 - page.style.close()
1.1557 -
1.1558 - def show_calendar_participant_headings(self, group_types, group_sources, group_columns):
1.1559 -
1.1560 - """
1.1561 - Show headings for the participants and other scheduling contributors,
1.1562 - defined by 'group_types', 'group_sources' and 'group_columns'.
1.1563 - """
1.1564 -
1.1565 - page = self.page
1.1566 -
1.1567 - page.colgroup(span=1, id="columns-timeslot")
1.1568 -
1.1569 - for group_type, columns in zip(group_types, group_columns):
1.1570 - page.colgroup(span=max(columns, 1), id="columns-%s" % group_type)
1.1571 -
1.1572 - page.thead()
1.1573 - page.tr()
1.1574 - page.th("", class_="emptyheading")
1.1575 -
1.1576 - for group_type, source, columns in zip(group_types, group_sources, group_columns):
1.1577 - page.th(source,
1.1578 - class_=(group_type == "request" and "requestheading" or "participantheading"),
1.1579 - colspan=max(columns, 1))
1.1580 -
1.1581 - page.tr.close()
1.1582 - page.thead.close()
1.1583 -
1.1584 - def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns):
1.1585 -
1.1586 - """
1.1587 - Show calendar days, defined by a collection of 'days', the contributing
1.1588 - period information as 'partitioned_groups' (partitioned by day), the
1.1589 - 'partitioned_group_types' indicating the kind of contribution involved,
1.1590 - and the 'group_columns' defining the number of columns in each group.
1.1591 - """
1.1592 -
1.1593 - page = self.page
1.1594 -
1.1595 - # Determine the number of columns required. Where participants provide
1.1596 - # no columns for events, one still needs to be provided for the
1.1597 - # participant itself.
1.1598 -
1.1599 - all_columns = sum([max(columns, 1) for columns in group_columns])
1.1600 -
1.1601 - # Determine the days providing time slots.
1.1602 -
1.1603 - all_days = days.items()
1.1604 - all_days.sort()
1.1605 -
1.1606 - # Produce a heading and time points for each day.
1.1607 -
1.1608 - for day, intervals in all_days:
1.1609 - groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
1.1610 - is_empty = True
1.1611 -
1.1612 - for slots in groups_for_day:
1.1613 - if not slots:
1.1614 - continue
1.1615 -
1.1616 - for active in slots.values():
1.1617 - if active:
1.1618 - is_empty = False
1.1619 - break
1.1620 -
1.1621 - page.thead(class_="separator%s" % (is_empty and " empty" or ""))
1.1622 - page.tr()
1.1623 - page.th(class_="dayheading container", colspan=all_columns+1)
1.1624 - self._day_heading(day)
1.1625 - page.th.close()
1.1626 - page.tr.close()
1.1627 - page.thead.close()
1.1628 -
1.1629 - page.tbody(class_="points%s" % (is_empty and " empty" or ""))
1.1630 - self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns)
1.1631 - page.tbody.close()
1.1632 -
1.1633 - def show_calendar_points(self, intervals, groups, group_types, group_columns):
1.1634 -
1.1635 - """
1.1636 - Show the time 'intervals' along with period information from the given
1.1637 - 'groups', having the indicated 'group_types', each with the number of
1.1638 - columns given by 'group_columns'.
1.1639 - """
1.1640 -
1.1641 - page = self.page
1.1642 -
1.1643 - # Obtain the user's timezone.
1.1644 -
1.1645 - tzid = self.get_tzid()
1.1646 -
1.1647 - # Produce a row for each interval.
1.1648 -
1.1649 - intervals = list(intervals)
1.1650 - intervals.sort()
1.1651 -
1.1652 - for point, endpoint in intervals:
1.1653 - continuation = point == get_start_of_day(point, tzid)
1.1654 -
1.1655 - # Some rows contain no period details and are marked as such.
1.1656 -
1.1657 - have_active = reduce(lambda x, y: x or y, [slots and slots.get(point) for slots in groups], None)
1.1658 -
1.1659 - css = " ".join([
1.1660 - "slot",
1.1661 - have_active and "busy" or "empty",
1.1662 - continuation and "daystart" or ""
1.1663 - ])
1.1664 -
1.1665 - page.tr(class_=css)
1.1666 - page.th(class_="timeslot")
1.1667 - self._time_point(point, endpoint)
1.1668 - page.th.close()
1.1669 -
1.1670 - # Obtain slots for the time point from each group.
1.1671 -
1.1672 - for columns, slots, group_type in zip(group_columns, groups, group_types):
1.1673 - active = slots and slots.get(point)
1.1674 -
1.1675 - # Where no periods exist for the given time interval, generate
1.1676 - # an empty cell. Where a participant provides no periods at all,
1.1677 - # the colspan is adjusted to be 1, not 0.
1.1678 -
1.1679 - if not active:
1.1680 - page.td(class_="empty container", colspan=max(columns, 1))
1.1681 - self._empty_slot(point, endpoint)
1.1682 - page.td.close()
1.1683 - continue
1.1684 -
1.1685 - slots = slots.items()
1.1686 - slots.sort()
1.1687 - spans = get_spans(slots)
1.1688 -
1.1689 - empty = 0
1.1690 -
1.1691 - # Show a column for each active period.
1.1692 -
1.1693 - for t in active:
1.1694 - if t and len(t) >= 2:
1.1695 -
1.1696 - # Flush empty slots preceding this one.
1.1697 -
1.1698 - if empty:
1.1699 - page.td(class_="empty container", colspan=empty)
1.1700 - self._empty_slot(point, endpoint)
1.1701 - page.td.close()
1.1702 - empty = 0
1.1703 -
1.1704 - start, end, uid, recurrenceid, summary, organiser, key = get_freebusy_details(t)
1.1705 - span = spans[key]
1.1706 -
1.1707 - # Produce a table cell only at the start of the period
1.1708 - # or when continued at the start of a day.
1.1709 -
1.1710 - if point == start or continuation:
1.1711 -
1.1712 - has_continued = continuation and point != start
1.1713 - will_continue = not ends_on_same_day(point, end, tzid)
1.1714 - is_organiser = organiser == self.user
1.1715 -
1.1716 - css = " ".join([
1.1717 - "event",
1.1718 - has_continued and "continued" or "",
1.1719 - will_continue and "continues" or "",
1.1720 - is_organiser and "organising" or "attending"
1.1721 - ])
1.1722 -
1.1723 - # Only anchor the first cell of events.
1.1724 - # Need to only anchor the first period for a recurring
1.1725 - # event.
1.1726 -
1.1727 - html_id = "%s-%s-%s" % (group_type, uid, recurrenceid or "")
1.1728 -
1.1729 - if point == start and html_id not in self.html_ids:
1.1730 - page.td(class_=css, rowspan=span, id=html_id)
1.1731 - self.html_ids.add(html_id)
1.1732 - else:
1.1733 - page.td(class_=css, rowspan=span)
1.1734 -
1.1735 - # Only link to events if they are not being
1.1736 - # updated by requests.
1.1737 -
1.1738 - if not summary or (uid, recurrenceid) in self._get_requests() and group_type != "request":
1.1739 - page.span(summary or "(Participant is busy)")
1.1740 - else:
1.1741 - page.a(summary, href=self.link_to(uid, recurrenceid))
1.1742 -
1.1743 - page.td.close()
1.1744 - else:
1.1745 - empty += 1
1.1746 -
1.1747 - # Pad with empty columns.
1.1748 -
1.1749 - empty = columns - len(active)
1.1750 -
1.1751 - if empty:
1.1752 - page.td(class_="empty container", colspan=empty)
1.1753 - self._empty_slot(point, endpoint)
1.1754 - page.td.close()
1.1755 -
1.1756 - page.tr.close()
1.1757 -
1.1758 - def _day_heading(self, day):
1.1759 -
1.1760 - """
1.1761 - Generate a heading for 'day' of the following form:
1.1762 -
1.1763 - <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label>
1.1764 - """
1.1765 -
1.1766 - page = self.page
1.1767 - daystr = format_datetime(day)
1.1768 - value, identifier = self._day_value_and_identifier(day)
1.1769 - page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier)
1.1770 -
1.1771 - def _time_point(self, point, endpoint):
1.1772 -
1.1773 - """
1.1774 - Generate headings for the 'point' to 'endpoint' period of the following
1.1775 - form:
1.1776 -
1.1777 - <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>
1.1778 - <span class="endpoint">10:00:00 CET</span>
1.1779 - """
1.1780 -
1.1781 - page = self.page
1.1782 - tzid = self.get_tzid()
1.1783 - daystr = format_datetime(point.date())
1.1784 - value, identifier = self._slot_value_and_identifier(point, endpoint)
1.1785 - slots = self.env.get_args().get("slot", [])
1.1786 - self._slot_selector(value, identifier, slots)
1.1787 - page.label(self.format_time(point, "long"), class_="timepoint day-%s" % daystr, for_=identifier)
1.1788 - page.span(self.format_time(endpoint or get_end_of_day(point, tzid), "long"), class_="endpoint")
1.1789 -
1.1790 - def _slot_selector(self, value, identifier, slots):
1.1791 -
1.1792 - """
1.1793 - Provide a timeslot control having the given 'value', employing the
1.1794 - indicated HTML 'identifier', and using the given 'slots' collection
1.1795 - to select any control whose 'value' is in this collection, unless the
1.1796 - "reset" request parameter has been asserted.
1.1797 - """
1.1798 -
1.1799 - reset = self.env.get_args().has_key("reset")
1.1800 - page = self.page
1.1801 - if not reset and value in slots:
1.1802 - page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector", checked="checked")
1.1803 - else:
1.1804 - page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector")
1.1805 -
1.1806 - def _empty_slot(self, point, endpoint):
1.1807 -
1.1808 - "Show an empty slot label for the given 'point' and 'endpoint'."
1.1809 -
1.1810 - page = self.page
1.1811 - value, identifier = self._slot_value_and_identifier(point, endpoint)
1.1812 - page.label("Select/deselect period", class_="newevent popup", for_=identifier)
1.1813 -
1.1814 - def _day_value_and_identifier(self, day):
1.1815 -
1.1816 - "Return a day value and HTML identifier for the given 'day'."
1.1817 -
1.1818 - value = "%s-" % format_datetime(day)
1.1819 - identifier = "day-%s" % value
1.1820 - return value, identifier
1.1821 -
1.1822 - def _slot_value_and_identifier(self, point, endpoint):
1.1823 -
1.1824 - """
1.1825 - Return a slot value and HTML identifier for the given 'point' and
1.1826 - 'endpoint'.
1.1827 - """
1.1828 -
1.1829 - value = "%s-%s" % (format_datetime(point), endpoint and format_datetime(endpoint) or "")
1.1830 - identifier = "slot-%s" % value
1.1831 - return value, identifier
1.1832 -
1.1833 - def _show_menu(self, name, default, items, class_="", index=None):
1.1834 -
1.1835 - """
1.1836 - Show a select menu having the given 'name', set to the given 'default',
1.1837 - providing the given (value, label) 'items', and employing the given CSS
1.1838 - 'class_' if specified.
1.1839 - """
1.1840 -
1.1841 - page = self.page
1.1842 - values = self.env.get_args().get(name, [default])
1.1843 - if index is not None:
1.1844 - values = values[index:]
1.1845 - values = values and values[0:1] or [default]
1.1846 -
1.1847 - page.select(name=name, class_=class_)
1.1848 - for v, label in items:
1.1849 - if v is None:
1.1850 - continue
1.1851 - if v in values:
1.1852 - page.option(label, value=v, selected="selected")
1.1853 - else:
1.1854 - page.option(label, value=v)
1.1855 - page.select.close()
1.1856 -
1.1857 - def _show_date_controls(self, name, default, tzid, index=None):
1.1858 -
1.1859 - """
1.1860 - Show date controls for a field with the given 'name' and 'default' value
1.1861 - and 'tzid'.
1.1862 - """
1.1863 -
1.1864 - page = self.page
1.1865 - args = self.env.get_args()
1.1866 -
1.1867 - event_tzid = tzid or self.get_tzid()
1.1868 -
1.1869 - # Show dates for up to one week around the current date.
1.1870 -
1.1871 - base = to_date(default)
1.1872 - items = []
1.1873 - for i in range(-7, 8):
1.1874 - d = base + timedelta(i)
1.1875 - items.append((format_datetime(d), self.format_date(d, "full")))
1.1876 -
1.1877 - self._show_menu("%s-date" % name, format_datetime(base), items, index=index)
1.1878 -
1.1879 - # Show time details.
1.1880 -
1.1881 - default_time = isinstance(default, datetime) and default or None
1.1882 -
1.1883 - hour = args.get("%s-hour" % name, [])[index or 0:]
1.1884 - hour = hour and hour[0] or "%02d" % (default_time and default_time.hour or 0)
1.1885 - minute = args.get("%s-minute" % name, [])[index or 0:]
1.1886 - minute = minute and minute[0] or "%02d" % (default_time and default_time.minute or 0)
1.1887 - second = args.get("%s-second" % name, [])[index or 0:]
1.1888 - second = second and second[0] or "%02d" % (default_time and default_time.second or 0)
1.1889 -
1.1890 - page.span(class_="time enabled")
1.1891 - page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2)
1.1892 - page.add(":")
1.1893 - page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2)
1.1894 - page.add(":")
1.1895 - page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2)
1.1896 - page.add(" ")
1.1897 - self._show_timezone_menu("%s-tzid" % name, event_tzid, index)
1.1898 - page.span.close()
1.1899 -
1.1900 - def _show_timezone_menu(self, name, default, index=None):
1.1901 -
1.1902 - """
1.1903 - Show timezone controls using a menu with the given 'name', set to the
1.1904 - given 'default' unless a field of the given 'name' provides a value.
1.1905 - """
1.1906 -
1.1907 - entries = [(tzid, tzid) for tzid in pytz.all_timezones]
1.1908 - self._show_menu(name, default, entries, index=index)
1.1909 -
1.1910 - # Incoming HTTP request direction.
1.1911 -
1.1912 def select_action(self):
1.1913
1.1914 "Select the desired action and show the result."
1.1915 @@ -1932,8 +42,8 @@
1.1916 path_info = self.env.get_path_info().strip("/")
1.1917
1.1918 if not path_info:
1.1919 - self.show_calendar()
1.1920 - elif self.show_object(path_info):
1.1921 + CalendarPage(self).show()
1.1922 + elif EventPage(self).show(path_info):
1.1923 pass
1.1924 else:
1.1925 self.no_page()