1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imipweb/calendar.py Thu Mar 26 16:11:46 2015 +0100
1.3 @@ -0,0 +1,722 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +A Web interface to an event calendar.
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
1.26 +from imiptools.data import get_address, get_uri, uri_values
1.27 +from imiptools.dates import format_datetime, get_datetime, \
1.28 + get_datetime_item, get_end_of_day, get_start_of_day, \
1.29 + get_start_of_next_day, get_timestamp, ends_on_same_day, \
1.30 + to_timezone
1.31 +from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
1.32 + convert_periods, get_freebusy_details, \
1.33 + get_scale, get_slots, get_spans, partition_by_day
1.34 +from imipweb.resource import Resource
1.35 +
1.36 +class CalendarPage(Resource):
1.37 +
1.38 + "A request handler for the calendar page."
1.39 +
1.40 + # Request logic methods.
1.41 +
1.42 + def handle_newevent(self):
1.43 +
1.44 + """
1.45 + Handle any new event operation, creating a new event and redirecting to
1.46 + the event page for further activity.
1.47 + """
1.48 +
1.49 + # Handle a submitted form.
1.50 +
1.51 + args = self.env.get_args()
1.52 +
1.53 + if not args.has_key("newevent"):
1.54 + return
1.55 +
1.56 + # Create a new event using the available information.
1.57 +
1.58 + slots = args.get("slot", [])
1.59 + participants = args.get("participants", [])
1.60 +
1.61 + if not slots:
1.62 + return
1.63 +
1.64 + # Obtain the user's timezone.
1.65 +
1.66 + tzid = self.get_tzid()
1.67 +
1.68 + # Coalesce the selected slots.
1.69 +
1.70 + slots.sort()
1.71 + coalesced = []
1.72 + last = None
1.73 +
1.74 + for slot in slots:
1.75 + start, end = slot.split("-")
1.76 + start = get_datetime(start, {"TZID" : tzid})
1.77 + end = end and get_datetime(end, {"TZID" : tzid}) or get_start_of_next_day(start, tzid)
1.78 +
1.79 + if last:
1.80 + last_start, last_end = last
1.81 +
1.82 + # Merge adjacent dates and datetimes.
1.83 +
1.84 + if start == last_end or \
1.85 + not isinstance(start, datetime) and \
1.86 + get_start_of_day(last_end, tzid) == get_start_of_day(start, tzid):
1.87 +
1.88 + last = last_start, end
1.89 + continue
1.90 +
1.91 + # Handle datetimes within dates.
1.92 + # Datetime periods are within single days and are therefore
1.93 + # discarded.
1.94 +
1.95 + elif not isinstance(last_start, datetime) and \
1.96 + get_start_of_day(start, tzid) == get_start_of_day(last_start, tzid):
1.97 +
1.98 + continue
1.99 +
1.100 + # Add separate dates and datetimes.
1.101 +
1.102 + else:
1.103 + coalesced.append(last)
1.104 +
1.105 + last = start, end
1.106 +
1.107 + if last:
1.108 + coalesced.append(last)
1.109 +
1.110 + # Invent a unique identifier.
1.111 +
1.112 + utcnow = get_timestamp()
1.113 + uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
1.114 +
1.115 + # Create a calendar object and store it as a request.
1.116 +
1.117 + record = []
1.118 + rwrite = record.append
1.119 +
1.120 + # Define a single occurrence if only one coalesced slot exists.
1.121 +
1.122 + start, end = coalesced[0]
1.123 + start_value, start_attr = get_datetime_item(start, tzid)
1.124 + end_value, end_attr = get_datetime_item(end, tzid)
1.125 +
1.126 + rwrite(("UID", {}, uid))
1.127 + rwrite(("SUMMARY", {}, "New event at %s" % utcnow))
1.128 + rwrite(("DTSTAMP", {}, utcnow))
1.129 + rwrite(("DTSTART", start_attr, start_value))
1.130 + rwrite(("DTEND", end_attr, end_value))
1.131 + rwrite(("ORGANIZER", {}, self.user))
1.132 +
1.133 + participants = uri_values(filter(None, participants))
1.134 +
1.135 + for participant in participants:
1.136 + rwrite(("ATTENDEE", {"RSVP" : "TRUE", "PARTSTAT" : "NEEDS-ACTION"}, participant))
1.137 +
1.138 + if self.user not in participants:
1.139 + rwrite(("ATTENDEE", {"PARTSTAT" : "ACCEPTED"}, self.user))
1.140 +
1.141 + # Define additional occurrences if many slots are defined.
1.142 +
1.143 + rdates = []
1.144 +
1.145 + for start, end in coalesced[1:]:
1.146 + start_value, start_attr = get_datetime_item(start, tzid)
1.147 + end_value, end_attr = get_datetime_item(end, tzid)
1.148 + rdates.append("%s/%s" % (start_value, end_value))
1.149 +
1.150 + if rdates:
1.151 + rwrite(("RDATE", {"VALUE" : "PERIOD", "TZID" : tzid}, rdates))
1.152 +
1.153 + node = ("VEVENT", {}, record)
1.154 +
1.155 + self.store.set_event(self.user, uid, None, node=node)
1.156 + self.store.queue_request(self.user, uid)
1.157 +
1.158 + # Redirect to the object (or the first of the objects), where instead of
1.159 + # attendee controls, there will be organiser controls.
1.160 +
1.161 + self.redirect(self.link_to(uid))
1.162 +
1.163 + # Page fragment methods.
1.164 +
1.165 + def show_requests_on_page(self):
1.166 +
1.167 + "Show requests for the current user."
1.168 +
1.169 + page = self.page
1.170 +
1.171 + # NOTE: This list could be more informative, but it is envisaged that
1.172 + # NOTE: the requests would be visited directly anyway.
1.173 +
1.174 + requests = self._get_requests()
1.175 +
1.176 + page.div(id="pending-requests")
1.177 +
1.178 + if requests:
1.179 + page.p("Pending requests:")
1.180 +
1.181 + page.ul()
1.182 +
1.183 + for uid, recurrenceid in requests:
1.184 + obj = self._get_object(uid, recurrenceid)
1.185 + if obj:
1.186 + page.li()
1.187 + page.a(obj.get_value("SUMMARY"), href="#request-%s-%s" % (uid, recurrenceid or ""))
1.188 + page.li.close()
1.189 +
1.190 + page.ul.close()
1.191 +
1.192 + else:
1.193 + page.p("There are no pending requests.")
1.194 +
1.195 + page.div.close()
1.196 +
1.197 + def show_participants_on_page(self):
1.198 +
1.199 + "Show participants for scheduling purposes."
1.200 +
1.201 + page = self.page
1.202 + args = self.env.get_args()
1.203 + participants = args.get("participants", [])
1.204 +
1.205 + try:
1.206 + for name, value in args.items():
1.207 + if name.startswith("remove-participant-"):
1.208 + i = int(name[len("remove-participant-"):])
1.209 + del participants[i]
1.210 + break
1.211 + except ValueError:
1.212 + pass
1.213 +
1.214 + # Trim empty participants.
1.215 +
1.216 + while participants and not participants[-1].strip():
1.217 + participants.pop()
1.218 +
1.219 + # Show any specified participants together with controls to remove and
1.220 + # add participants.
1.221 +
1.222 + page.div(id="participants")
1.223 +
1.224 + page.p("Participants for scheduling:")
1.225 +
1.226 + for i, participant in enumerate(participants):
1.227 + page.p()
1.228 + page.input(name="participants", type="text", value=participant)
1.229 + page.input(name="remove-participant-%d" % i, type="submit", value="Remove")
1.230 + page.p.close()
1.231 +
1.232 + page.p()
1.233 + page.input(name="participants", type="text")
1.234 + page.input(name="add-participant", type="submit", value="Add")
1.235 + page.p.close()
1.236 +
1.237 + page.div.close()
1.238 +
1.239 + return participants
1.240 +
1.241 + # Full page output methods.
1.242 +
1.243 + def show(self):
1.244 +
1.245 + "Show the calendar for the current user."
1.246 +
1.247 + handled = self.handle_newevent()
1.248 +
1.249 + self.new_page(title="Calendar")
1.250 + page = self.page
1.251 +
1.252 + # Form controls are used in various places on the calendar page.
1.253 +
1.254 + page.form(method="POST")
1.255 +
1.256 + self.show_requests_on_page()
1.257 + participants = self.show_participants_on_page()
1.258 +
1.259 + # Show a button for scheduling a new event.
1.260 +
1.261 + page.p(class_="controls")
1.262 + page.input(name="newevent", type="submit", value="New event", id="newevent", accesskey="N")
1.263 + page.p.close()
1.264 +
1.265 + # Show controls for hiding empty days and busy slots.
1.266 + # The positioning of the control, paragraph and table are important here.
1.267 +
1.268 + page.input(name="showdays", type="checkbox", value="show", id="showdays", accesskey="D")
1.269 + page.input(name="hidebusy", type="checkbox", value="hide", id="hidebusy", accesskey="B")
1.270 +
1.271 + page.p(class_="controls")
1.272 + page.label("Hide busy time periods", for_="hidebusy", class_="hidebusy enable")
1.273 + page.label("Show busy time periods", for_="hidebusy", class_="hidebusy disable")
1.274 + page.label("Show empty days", for_="showdays", class_="showdays disable")
1.275 + page.label("Hide empty days", for_="showdays", class_="showdays enable")
1.276 + page.input(name="reset", type="submit", value="Clear selections", id="reset")
1.277 + page.label("Clear selections", for_="reset", class_="reset")
1.278 + page.p.close()
1.279 +
1.280 + freebusy = self.store.get_freebusy(self.user)
1.281 +
1.282 + if not freebusy:
1.283 + page.p("No events scheduled.")
1.284 + return
1.285 +
1.286 + # Obtain the user's timezone.
1.287 +
1.288 + tzid = self.get_tzid()
1.289 +
1.290 + # Day view: start at the earliest known day and produce days until the
1.291 + # latest known day, perhaps with expandable sections of empty days.
1.292 +
1.293 + # Month view: start at the earliest known month and produce months until
1.294 + # the latest known month, perhaps with expandable sections of empty
1.295 + # months.
1.296 +
1.297 + # Details of users to invite to new events could be superimposed on the
1.298 + # calendar.
1.299 +
1.300 + # Requests are listed and linked to their tentative positions in the
1.301 + # calendar. Other participants are also shown.
1.302 +
1.303 + request_summary = self._get_request_summary()
1.304 +
1.305 + period_groups = [request_summary, freebusy]
1.306 + period_group_types = ["request", "freebusy"]
1.307 + period_group_sources = ["Pending requests", "Your schedule"]
1.308 +
1.309 + for i, participant in enumerate(participants):
1.310 + period_groups.append(self.store.get_freebusy_for_other(self.user, get_uri(participant)))
1.311 + period_group_types.append("freebusy-part%d" % i)
1.312 + period_group_sources.append(participant)
1.313 +
1.314 + groups = []
1.315 + group_columns = []
1.316 + group_types = period_group_types
1.317 + group_sources = period_group_sources
1.318 + all_points = set()
1.319 +
1.320 + # Obtain time point information for each group of periods.
1.321 +
1.322 + for periods in period_groups:
1.323 + periods = convert_periods(periods, tzid)
1.324 +
1.325 + # Get the time scale with start and end points.
1.326 +
1.327 + scale = get_scale(periods)
1.328 +
1.329 + # Get the time slots for the periods.
1.330 +
1.331 + slots = get_slots(scale)
1.332 +
1.333 + # Add start of day time points for multi-day periods.
1.334 +
1.335 + add_day_start_points(slots, tzid)
1.336 +
1.337 + # Record the slots and all time points employed.
1.338 +
1.339 + groups.append(slots)
1.340 + all_points.update([point for point, active in slots])
1.341 +
1.342 + # Partition the groups into days.
1.343 +
1.344 + days = {}
1.345 + partitioned_groups = []
1.346 + partitioned_group_types = []
1.347 + partitioned_group_sources = []
1.348 +
1.349 + for slots, group_type, group_source in zip(groups, group_types, group_sources):
1.350 +
1.351 + # Propagate time points to all groups of time slots.
1.352 +
1.353 + add_slots(slots, all_points)
1.354 +
1.355 + # Count the number of columns employed by the group.
1.356 +
1.357 + columns = 0
1.358 +
1.359 + # Partition the time slots by day.
1.360 +
1.361 + partitioned = {}
1.362 +
1.363 + for day, day_slots in partition_by_day(slots).items():
1.364 +
1.365 + # Construct a list of time intervals within the day.
1.366 +
1.367 + intervals = []
1.368 + last = None
1.369 +
1.370 + for point, active in day_slots:
1.371 + columns = max(columns, len(active))
1.372 + if last:
1.373 + intervals.append((last, point))
1.374 + last = point
1.375 +
1.376 + if last:
1.377 + intervals.append((last, None))
1.378 +
1.379 + if not days.has_key(day):
1.380 + days[day] = set()
1.381 +
1.382 + # Convert each partition to a mapping from points to active
1.383 + # periods.
1.384 +
1.385 + partitioned[day] = dict(day_slots)
1.386 +
1.387 + # Record the divisions or intervals within each day.
1.388 +
1.389 + days[day].update(intervals)
1.390 +
1.391 + # Only include the requests column if it provides objects.
1.392 +
1.393 + if group_type != "request" or columns:
1.394 + group_columns.append(columns)
1.395 + partitioned_groups.append(partitioned)
1.396 + partitioned_group_types.append(group_type)
1.397 + partitioned_group_sources.append(group_source)
1.398 +
1.399 + # Add empty days.
1.400 +
1.401 + add_empty_days(days, tzid)
1.402 +
1.403 + # Show the controls permitting day selection.
1.404 +
1.405 + self.show_calendar_day_controls(days)
1.406 +
1.407 + # Show the calendar itself.
1.408 +
1.409 + page.table(cellspacing=5, cellpadding=5, class_="calendar")
1.410 + self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)
1.411 + self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns)
1.412 + page.table.close()
1.413 +
1.414 + # End the form region.
1.415 +
1.416 + page.form.close()
1.417 +
1.418 + # More page fragment methods.
1.419 +
1.420 + def show_calendar_day_controls(self, days):
1.421 +
1.422 + "Show controls for the given 'days' in the calendar."
1.423 +
1.424 + page = self.page
1.425 + slots = self.env.get_args().get("slot", [])
1.426 +
1.427 + for day in days:
1.428 + value, identifier = self._day_value_and_identifier(day)
1.429 + self._slot_selector(value, identifier, slots)
1.430 +
1.431 + # Generate a dynamic stylesheet to allow day selections to colour
1.432 + # specific days.
1.433 + # NOTE: The style details need to be coordinated with the static
1.434 + # NOTE: stylesheet.
1.435 +
1.436 + page.style(type="text/css")
1.437 +
1.438 + for day in days:
1.439 + daystr = format_datetime(day)
1.440 + page.add("""\
1.441 +input.newevent.selector#day-%s-:checked ~ table label.day.day-%s,
1.442 +input.newevent.selector#day-%s-:checked ~ table label.timepoint.day-%s {
1.443 + background-color: #5f4;
1.444 + text-decoration: underline;
1.445 +}
1.446 +""" % (daystr, daystr, daystr, daystr))
1.447 +
1.448 + page.style.close()
1.449 +
1.450 + def show_calendar_participant_headings(self, group_types, group_sources, group_columns):
1.451 +
1.452 + """
1.453 + Show headings for the participants and other scheduling contributors,
1.454 + defined by 'group_types', 'group_sources' and 'group_columns'.
1.455 + """
1.456 +
1.457 + page = self.page
1.458 +
1.459 + page.colgroup(span=1, id="columns-timeslot")
1.460 +
1.461 + for group_type, columns in zip(group_types, group_columns):
1.462 + page.colgroup(span=max(columns, 1), id="columns-%s" % group_type)
1.463 +
1.464 + page.thead()
1.465 + page.tr()
1.466 + page.th("", class_="emptyheading")
1.467 +
1.468 + for group_type, source, columns in zip(group_types, group_sources, group_columns):
1.469 + page.th(source,
1.470 + class_=(group_type == "request" and "requestheading" or "participantheading"),
1.471 + colspan=max(columns, 1))
1.472 +
1.473 + page.tr.close()
1.474 + page.thead.close()
1.475 +
1.476 + def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns):
1.477 +
1.478 + """
1.479 + Show calendar days, defined by a collection of 'days', the contributing
1.480 + period information as 'partitioned_groups' (partitioned by day), the
1.481 + 'partitioned_group_types' indicating the kind of contribution involved,
1.482 + and the 'group_columns' defining the number of columns in each group.
1.483 + """
1.484 +
1.485 + page = self.page
1.486 +
1.487 + # Determine the number of columns required. Where participants provide
1.488 + # no columns for events, one still needs to be provided for the
1.489 + # participant itself.
1.490 +
1.491 + all_columns = sum([max(columns, 1) for columns in group_columns])
1.492 +
1.493 + # Determine the days providing time slots.
1.494 +
1.495 + all_days = days.items()
1.496 + all_days.sort()
1.497 +
1.498 + # Produce a heading and time points for each day.
1.499 +
1.500 + for day, intervals in all_days:
1.501 + groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
1.502 + is_empty = True
1.503 +
1.504 + for slots in groups_for_day:
1.505 + if not slots:
1.506 + continue
1.507 +
1.508 + for active in slots.values():
1.509 + if active:
1.510 + is_empty = False
1.511 + break
1.512 +
1.513 + page.thead(class_="separator%s" % (is_empty and " empty" or ""))
1.514 + page.tr()
1.515 + page.th(class_="dayheading container", colspan=all_columns+1)
1.516 + self._day_heading(day)
1.517 + page.th.close()
1.518 + page.tr.close()
1.519 + page.thead.close()
1.520 +
1.521 + page.tbody(class_="points%s" % (is_empty and " empty" or ""))
1.522 + self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns)
1.523 + page.tbody.close()
1.524 +
1.525 + def show_calendar_points(self, intervals, groups, group_types, group_columns):
1.526 +
1.527 + """
1.528 + Show the time 'intervals' along with period information from the given
1.529 + 'groups', having the indicated 'group_types', each with the number of
1.530 + columns given by 'group_columns'.
1.531 + """
1.532 +
1.533 + page = self.page
1.534 +
1.535 + # Obtain the user's timezone.
1.536 +
1.537 + tzid = self.get_tzid()
1.538 +
1.539 + # Produce a row for each interval.
1.540 +
1.541 + intervals = list(intervals)
1.542 + intervals.sort()
1.543 +
1.544 + for point, endpoint in intervals:
1.545 + continuation = point == get_start_of_day(point, tzid)
1.546 +
1.547 + # Some rows contain no period details and are marked as such.
1.548 +
1.549 + have_active = reduce(lambda x, y: x or y, [slots and slots.get(point) for slots in groups], None)
1.550 +
1.551 + css = " ".join([
1.552 + "slot",
1.553 + have_active and "busy" or "empty",
1.554 + continuation and "daystart" or ""
1.555 + ])
1.556 +
1.557 + page.tr(class_=css)
1.558 + page.th(class_="timeslot")
1.559 + self._time_point(point, endpoint)
1.560 + page.th.close()
1.561 +
1.562 + # Obtain slots for the time point from each group.
1.563 +
1.564 + for columns, slots, group_type in zip(group_columns, groups, group_types):
1.565 + active = slots and slots.get(point)
1.566 +
1.567 + # Where no periods exist for the given time interval, generate
1.568 + # an empty cell. Where a participant provides no periods at all,
1.569 + # the colspan is adjusted to be 1, not 0.
1.570 +
1.571 + if not active:
1.572 + page.td(class_="empty container", colspan=max(columns, 1))
1.573 + self._empty_slot(point, endpoint)
1.574 + page.td.close()
1.575 + continue
1.576 +
1.577 + slots = slots.items()
1.578 + slots.sort()
1.579 + spans = get_spans(slots)
1.580 +
1.581 + empty = 0
1.582 +
1.583 + # Show a column for each active period.
1.584 +
1.585 + for t in active:
1.586 + if t and len(t) >= 2:
1.587 +
1.588 + # Flush empty slots preceding this one.
1.589 +
1.590 + if empty:
1.591 + page.td(class_="empty container", colspan=empty)
1.592 + self._empty_slot(point, endpoint)
1.593 + page.td.close()
1.594 + empty = 0
1.595 +
1.596 + start, end, uid, recurrenceid, summary, organiser, key = get_freebusy_details(t)
1.597 + span = spans[key]
1.598 +
1.599 + # Produce a table cell only at the start of the period
1.600 + # or when continued at the start of a day.
1.601 +
1.602 + if point == start or continuation:
1.603 +
1.604 + has_continued = continuation and point != start
1.605 + will_continue = not ends_on_same_day(point, end, tzid)
1.606 + is_organiser = organiser == self.user
1.607 +
1.608 + css = " ".join([
1.609 + "event",
1.610 + has_continued and "continued" or "",
1.611 + will_continue and "continues" or "",
1.612 + is_organiser and "organising" or "attending"
1.613 + ])
1.614 +
1.615 + # Only anchor the first cell of events.
1.616 + # Need to only anchor the first period for a recurring
1.617 + # event.
1.618 +
1.619 + html_id = "%s-%s-%s" % (group_type, uid, recurrenceid or "")
1.620 +
1.621 + if point == start and html_id not in self.html_ids:
1.622 + page.td(class_=css, rowspan=span, id=html_id)
1.623 + self.html_ids.add(html_id)
1.624 + else:
1.625 + page.td(class_=css, rowspan=span)
1.626 +
1.627 + # Only link to events if they are not being
1.628 + # updated by requests.
1.629 +
1.630 + if not summary or (uid, recurrenceid) in self._get_requests() and group_type != "request":
1.631 + page.span(summary or "(Participant is busy)")
1.632 + else:
1.633 + page.a(summary, href=self.link_to(uid, recurrenceid))
1.634 +
1.635 + page.td.close()
1.636 + else:
1.637 + empty += 1
1.638 +
1.639 + # Pad with empty columns.
1.640 +
1.641 + empty = columns - len(active)
1.642 +
1.643 + if empty:
1.644 + page.td(class_="empty container", colspan=empty)
1.645 + self._empty_slot(point, endpoint)
1.646 + page.td.close()
1.647 +
1.648 + page.tr.close()
1.649 +
1.650 + def _day_heading(self, day):
1.651 +
1.652 + """
1.653 + Generate a heading for 'day' of the following form:
1.654 +
1.655 + <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label>
1.656 + """
1.657 +
1.658 + page = self.page
1.659 + daystr = format_datetime(day)
1.660 + value, identifier = self._day_value_and_identifier(day)
1.661 + page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier)
1.662 +
1.663 + def _time_point(self, point, endpoint):
1.664 +
1.665 + """
1.666 + Generate headings for the 'point' to 'endpoint' period of the following
1.667 + form:
1.668 +
1.669 + <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>
1.670 + <span class="endpoint">10:00:00 CET</span>
1.671 + """
1.672 +
1.673 + page = self.page
1.674 + tzid = self.get_tzid()
1.675 + daystr = format_datetime(point.date())
1.676 + value, identifier = self._slot_value_and_identifier(point, endpoint)
1.677 + slots = self.env.get_args().get("slot", [])
1.678 + self._slot_selector(value, identifier, slots)
1.679 + page.label(self.format_time(point, "long"), class_="timepoint day-%s" % daystr, for_=identifier)
1.680 + page.span(self.format_time(endpoint or get_end_of_day(point, tzid), "long"), class_="endpoint")
1.681 +
1.682 + def _slot_selector(self, value, identifier, slots):
1.683 +
1.684 + """
1.685 + Provide a timeslot control having the given 'value', employing the
1.686 + indicated HTML 'identifier', and using the given 'slots' collection
1.687 + to select any control whose 'value' is in this collection, unless the
1.688 + "reset" request parameter has been asserted.
1.689 + """
1.690 +
1.691 + reset = self.env.get_args().has_key("reset")
1.692 + page = self.page
1.693 + if not reset and value in slots:
1.694 + page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector", checked="checked")
1.695 + else:
1.696 + page.input(name="slot", type="checkbox", value=value, id=identifier, class_="newevent selector")
1.697 +
1.698 + def _empty_slot(self, point, endpoint):
1.699 +
1.700 + "Show an empty slot label for the given 'point' and 'endpoint'."
1.701 +
1.702 + page = self.page
1.703 + value, identifier = self._slot_value_and_identifier(point, endpoint)
1.704 + page.label("Select/deselect period", class_="newevent popup", for_=identifier)
1.705 +
1.706 + def _day_value_and_identifier(self, day):
1.707 +
1.708 + "Return a day value and HTML identifier for the given 'day'."
1.709 +
1.710 + value = "%s-" % format_datetime(day)
1.711 + identifier = "day-%s" % value
1.712 + return value, identifier
1.713 +
1.714 + def _slot_value_and_identifier(self, point, endpoint):
1.715 +
1.716 + """
1.717 + Return a slot value and HTML identifier for the given 'point' and
1.718 + 'endpoint'.
1.719 + """
1.720 +
1.721 + value = "%s-%s" % (format_datetime(point), endpoint and format_datetime(endpoint) or "")
1.722 + identifier = "slot-%s" % value
1.723 + return value, identifier
1.724 +
1.725 +# vim: tabstop=4 expandtab shiftwidth=4