1.1 --- a/imipweb/event.py Thu Sep 24 20:41:58 2015 +0200
1.2 +++ b/imipweb/event.py Thu Sep 24 23:41:47 2015 +0200
1.3 @@ -23,19 +23,16 @@
1.4 from imiptools.dates import to_timezone
1.5 from imiptools.mail import Messenger
1.6 from imiptools.period import have_conflict
1.7 -from imipweb.data import EventPeriod, \
1.8 - event_period_from_period, form_period_from_period, \
1.9 - FormDate, FormPeriod, PeriodError
1.10 +from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError
1.11 from imipweb.client import ManagerClient
1.12 -from imipweb.resource import FormUtilities, ResourceClientForObject
1.13 +from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject
1.14
1.15 -class EventPage(ResourceClientForObject, FormUtilities):
1.16 -
1.17 - "A request handler for the event page."
1.18 +class EventPageFragment(ResourceClientForObject, DateTimeFormUtilities, FormUtilities):
1.19
1.20 - def __init__(self, resource=None, messenger=None):
1.21 + "A resource presenting the details of an event."
1.22 +
1.23 + def __init__(self, resource=None):
1.24 ResourceClientForObject.__init__(self, resource)
1.25 - self.messenger = messenger or Messenger()
1.26
1.27 # Various property values and labels.
1.28
1.29 @@ -56,323 +53,9 @@
1.30 (None, "Not indicated"),
1.31 ]
1.32
1.33 - # Access to stored object information.
1.34 -
1.35 def is_organiser(self):
1.36 return get_uri(self.obj.get_value("ORGANIZER")) == self.user
1.37
1.38 - def get_stored_attendees(self):
1.39 - return uri_values(self.obj.get_values("ATTENDEE") or [])
1.40 -
1.41 - def get_stored_main_period(self):
1.42 -
1.43 - "Return the main event period for the current object."
1.44 -
1.45 - (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid())
1.46 - return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr)
1.47 -
1.48 - def get_stored_recurrences(self):
1.49 -
1.50 - "Return recurrences computed using the current object."
1.51 -
1.52 - recurrences = []
1.53 - for period in self.get_periods(self.obj):
1.54 - if period.origin != "DTSTART":
1.55 - recurrences.append(period)
1.56 - return recurrences
1.57 -
1.58 - # Request logic methods.
1.59 -
1.60 - def is_initial_load(self):
1.61 -
1.62 - "Return whether the event is being loaded and shown for the first time."
1.63 -
1.64 - return not self.env.get_args().has_key("editing")
1.65 -
1.66 - def handle_request(self):
1.67 -
1.68 - """
1.69 - Handle actions involving the current object, returning an error if one
1.70 - occurred, or None if the request was successfully handled.
1.71 - """
1.72 -
1.73 - # Handle a submitted form.
1.74 -
1.75 - args = self.env.get_args()
1.76 -
1.77 - # Get the possible actions.
1.78 -
1.79 - reply = args.has_key("reply")
1.80 - discard = args.has_key("discard")
1.81 - create = args.has_key("create")
1.82 - cancel = args.has_key("cancel")
1.83 - ignore = args.has_key("ignore")
1.84 - save = args.has_key("save")
1.85 -
1.86 - have_action = reply or discard or create or cancel or ignore or save
1.87 -
1.88 - if not have_action:
1.89 - return ["action"]
1.90 -
1.91 - # If ignoring the object, return to the calendar.
1.92 -
1.93 - if ignore:
1.94 - self.redirect(self.env.get_path())
1.95 - return None
1.96 -
1.97 - # Update the object.
1.98 -
1.99 - single_user = False
1.100 -
1.101 - if reply or create or cancel or save:
1.102 -
1.103 - # Update principal event details if organiser.
1.104 -
1.105 - if self.is_organiser():
1.106 -
1.107 - # Update time periods (main and recurring).
1.108 -
1.109 - try:
1.110 - period = self.handle_main_period()
1.111 - except PeriodError, exc:
1.112 - return exc.args
1.113 -
1.114 - try:
1.115 - periods = self.handle_recurrence_periods()
1.116 - except PeriodError, exc:
1.117 - return exc.args
1.118 -
1.119 - # Set the periods in the object, first obtaining removed and
1.120 - # modified period information.
1.121 -
1.122 - to_unschedule = self.get_removed_periods()
1.123 -
1.124 - self.obj.set_period(period)
1.125 - self.obj.set_periods(periods)
1.126 -
1.127 - # Update summary.
1.128 -
1.129 - if args.has_key("summary"):
1.130 - self.obj["SUMMARY"] = [(args["summary"][0], {})]
1.131 -
1.132 - # Obtain any participants and those to be removed.
1.133 -
1.134 - attendees = self.get_attendees_from_page()
1.135 - removed = [attendees[int(i)] for i in args.get("remove", [])]
1.136 - to_cancel = self.update_attendees(self.obj, attendees, removed)
1.137 - single_user = not attendees or attendees == [self.user]
1.138 -
1.139 - # Update attendee participation for the current user.
1.140 -
1.141 - if args.has_key("partstat"):
1.142 - self.update_participation(self.obj, args["partstat"][0])
1.143 -
1.144 - # Process any action.
1.145 -
1.146 - invite = not save and create and not single_user
1.147 - save = save or create and single_user
1.148 -
1.149 - handled = True
1.150 -
1.151 - if reply or invite or cancel:
1.152 -
1.153 - client = ManagerClient(self.obj, self.user, self.messenger)
1.154 -
1.155 - # Process the object and remove it from the list of requests.
1.156 -
1.157 - if reply and client.process_received_request():
1.158 - self.remove_request(self.uid, self.recurrenceid)
1.159 -
1.160 - elif self.is_organiser() and (invite or cancel):
1.161 -
1.162 - # Invitation, uninvitation and unscheduling...
1.163 -
1.164 - if client.process_created_request(
1.165 - invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule):
1.166 -
1.167 - self.remove_request(self.uid, self.recurrenceid)
1.168 -
1.169 - # Save single user events.
1.170 -
1.171 - elif save:
1.172 - self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node())
1.173 - self.update_event_in_freebusy()
1.174 - self.remove_request(self.uid, self.recurrenceid)
1.175 -
1.176 - # Remove the request and the object.
1.177 -
1.178 - elif discard:
1.179 - self.remove_event_from_freebusy()
1.180 - self.remove_event(self.uid, self.recurrenceid)
1.181 - self.remove_request(self.uid, self.recurrenceid)
1.182 -
1.183 - else:
1.184 - handled = False
1.185 -
1.186 - # Upon handling an action, redirect to the main page.
1.187 -
1.188 - if handled:
1.189 - self.redirect(self.env.get_path())
1.190 -
1.191 - return None
1.192 -
1.193 - def handle_main_period(self):
1.194 -
1.195 - "Return period details for the main start/end period in an event."
1.196 -
1.197 - return self.get_main_period().as_event_period()
1.198 -
1.199 - def handle_recurrence_periods(self):
1.200 -
1.201 - "Return period details for the recurrences specified for an event."
1.202 -
1.203 - return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences())]
1.204 -
1.205 - def get_date_control_values(self, name, multiple=False, tzid_name=None):
1.206 -
1.207 - """
1.208 - Return a dictionary containing date, time and tzid entries for fields
1.209 - starting with 'name'. If 'multiple' is set to a true value, many
1.210 - dictionaries will be returned corresponding to a collection of
1.211 - datetimes. If 'tzid_name' is specified, the time zone information will
1.212 - be acquired from a field starting with 'tzid_name' instead of 'name'.
1.213 - """
1.214 -
1.215 - args = self.env.get_args()
1.216 -
1.217 - dates = args.get("%s-date" % name, [])
1.218 - hours = args.get("%s-hour" % name, [])
1.219 - minutes = args.get("%s-minute" % name, [])
1.220 - seconds = args.get("%s-second" % name, [])
1.221 - tzids = args.get("%s-tzid" % (tzid_name or name), [])
1.222 -
1.223 - # Handle absent values by employing None values.
1.224 -
1.225 - field_values = map(None, dates, hours, minutes, seconds, tzids)
1.226 -
1.227 - if not field_values and not multiple:
1.228 - all_values = FormDate()
1.229 - else:
1.230 - all_values = []
1.231 - for date, hour, minute, second, tzid in field_values:
1.232 - value = FormDate(date, hour, minute, second, tzid or self.get_tzid())
1.233 -
1.234 - # Return a single value or append to a collection of all values.
1.235 -
1.236 - if not multiple:
1.237 - return value
1.238 - else:
1.239 - all_values.append(value)
1.240 -
1.241 - return all_values
1.242 -
1.243 - def get_current_main_period(self):
1.244 -
1.245 - """
1.246 - Return the currently active main period for the current object depending
1.247 - on whether editing has begun or whether the object has just been loaded.
1.248 - """
1.249 -
1.250 - if self.is_initial_load() or not self.is_organiser():
1.251 - return self.get_stored_main_period()
1.252 - else:
1.253 - return self.get_main_period()
1.254 -
1.255 - def get_main_period(self):
1.256 -
1.257 - "Return the main period defined in the event form."
1.258 -
1.259 - args = self.env.get_args()
1.260 -
1.261 - dtend_enabled = args.get("dtend-control", [None])[0]
1.262 - dttimes_enabled = args.get("dttimes-control", [None])[0]
1.263 - start = self.get_date_control_values("dtstart")
1.264 - end = self.get_date_control_values("dtend")
1.265 -
1.266 - return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid())
1.267 -
1.268 - def get_current_recurrences(self):
1.269 -
1.270 - """
1.271 - Return recurrences for the current object using the original object
1.272 - details where no editing is in progress, using form data otherwise.
1.273 - """
1.274 -
1.275 - if self.is_initial_load() or not self.is_organiser():
1.276 - return self.get_stored_recurrences()
1.277 - else:
1.278 - return self.get_recurrences()
1.279 -
1.280 - def get_recurrences(self):
1.281 -
1.282 - "Return the recurrences defined in the event form."
1.283 -
1.284 - args = self.env.get_args()
1.285 -
1.286 - all_dtend_enabled = args.get("dtend-control-recur", [])
1.287 - all_dttimes_enabled = args.get("dttimes-control-recur", [])
1.288 - all_starts = self.get_date_control_values("dtstart-recur", multiple=True)
1.289 - all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")
1.290 - all_origins = args.get("recur-origin", [])
1.291 -
1.292 - periods = []
1.293 -
1.294 - for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \
1.295 - enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)):
1.296 -
1.297 - dtend_enabled = str(index) in all_dtend_enabled
1.298 - dttimes_enabled = str(index) in all_dttimes_enabled
1.299 - period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin)
1.300 - periods.append(period)
1.301 -
1.302 - return periods
1.303 -
1.304 - def get_removed_periods(self):
1.305 -
1.306 - "Return a list of recurrence periods to remove upon updating an event."
1.307 -
1.308 - to_unschedule = []
1.309 - args = self.env.get_args()
1.310 - for i in args.get("recur-remove", []):
1.311 - to_unschedule.append(periods[int(i)])
1.312 - return to_unschedule
1.313 -
1.314 - def get_current_attendees(self):
1.315 -
1.316 - """
1.317 - Return attendees for the current object depending on whether the object
1.318 - has been edited or instead provides such information from its stored
1.319 - form.
1.320 - """
1.321 -
1.322 - if self.is_initial_load() or not self.is_organiser():
1.323 - return self.get_stored_attendees()
1.324 - else:
1.325 - return self.get_attendees_from_page()
1.326 -
1.327 - def get_attendees_from_page(self):
1.328 -
1.329 - """
1.330 - Return attendees from the request, normalised for iCalendar purposes,
1.331 - and without duplicates.
1.332 - """
1.333 -
1.334 - args = self.env.get_args()
1.335 -
1.336 - attendees = args.get("attendee", [])
1.337 - unique_attendees = set()
1.338 - ordered_attendees = []
1.339 -
1.340 - for attendee in attendees:
1.341 - if not attendee.strip():
1.342 - continue
1.343 - attendee = get_uri(attendee)
1.344 - if attendee not in unique_attendees:
1.345 - unique_attendees.add(attendee)
1.346 - ordered_attendees.append(attendee)
1.347 -
1.348 - return ordered_attendees
1.349 -
1.350 def can_remove_attendee(self, attendee):
1.351
1.352 """
1.353 @@ -394,37 +77,38 @@
1.354
1.355 return attendee not in self.get_stored_attendees()
1.356
1.357 - def update_attendees_from_page(self):
1.358 + # Access to stored object information.
1.359
1.360 - "Add or remove attendees. This does not affect the stored object."
1.361 + def get_stored_attendees(self):
1.362 + return uri_values(self.obj.get_values("ATTENDEE") or [])
1.363
1.364 - args = self.env.get_args()
1.365 + def get_stored_main_period(self):
1.366
1.367 - attendees = self.get_attendees_from_page()
1.368 + "Return the main event period for the current object."
1.369
1.370 - if args.has_key("add"):
1.371 - attendees.append("")
1.372 + (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid())
1.373 + return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr)
1.374
1.375 - # Only actually remove attendees if the event is unsent, if the attendee
1.376 - # is new, or if it is the current user being removed.
1.377 + def get_stored_recurrences(self):
1.378 +
1.379 + "Return recurrences computed using the current object."
1.380
1.381 - if args.has_key("remove"):
1.382 - still_to_remove = []
1.383 + recurrences = []
1.384 + for period in self.get_periods(self.obj):
1.385 + if period.origin != "DTSTART":
1.386 + recurrences.append(period)
1.387 + return recurrences
1.388
1.389 - for i in args["remove"]:
1.390 - try:
1.391 - attendee = attendees[int(i)]
1.392 - except IndexError:
1.393 - continue
1.394 + # Access to current object information.
1.395
1.396 - if self.can_remove_attendee(attendee):
1.397 - attendees.remove(attendee)
1.398 - else:
1.399 - still_to_remove.append(i)
1.400 + def get_current_main_period(self):
1.401 + return self.get_stored_main_period()
1.402
1.403 - args["remove"] = still_to_remove
1.404 + def get_current_recurrences(self):
1.405 + return self.get_stored_recurrences()
1.406
1.407 - return attendees
1.408 + def get_current_attendees(self):
1.409 + return self.get_stored_attendees()
1.410
1.411 # Page fragment methods.
1.412
1.413 @@ -486,18 +170,14 @@
1.414
1.415 # Obtain basic event information, generating any necessary editing controls.
1.416
1.417 - if self.is_initial_load() or not self.is_organiser():
1.418 - attendees = self.get_stored_attendees()
1.419 - else:
1.420 - attendees = self.update_attendees_from_page()
1.421 -
1.422 - p = self.get_current_main_period()
1.423 - self.show_object_datetime_controls(p)
1.424 + attendees = self.update_current_attendees()
1.425 + period = self.get_current_main_period()
1.426 + self.show_object_datetime_controls(period)
1.427
1.428 # Obtain any separate recurrences for this event.
1.429
1.430 recurrenceids = self._get_active_recurrences(self.uid)
1.431 - replaced = not self.recurrenceid and p.is_replaced(recurrenceids)
1.432 + replaced = not self.recurrenceid and period.is_replaced(recurrenceids)
1.433
1.434 # Provide a summary of the object.
1.435
1.436 @@ -536,7 +216,7 @@
1.437 # basis of any potential datetime specified if dt-control is
1.438 # set.
1.439
1.440 - self.show_datetime_controls(is_start and p.get_form_start() or p.get_form_end(), is_start)
1.441 + self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start)
1.442
1.443 elif name == "DTSTART":
1.444 page.td(class_="objectvalue %s replaced" % field, rowspan=2)
1.445 @@ -891,188 +571,312 @@
1.446 page.tbody.close()
1.447 page.table.close()
1.448
1.449 - # Generation of controls within page fragments.
1.450 +class EventPage(EventPageFragment):
1.451 +
1.452 + "A request handler for the event page."
1.453 +
1.454 + def __init__(self, resource=None, messenger=None):
1.455 + ResourceClientForObject.__init__(self, resource)
1.456 + self.messenger = messenger or Messenger()
1.457
1.458 - def show_object_datetime_controls(self, period, index=None):
1.459 + # Request logic methods.
1.460 +
1.461 + def is_initial_load(self):
1.462 +
1.463 + "Return whether the event is being loaded and shown for the first time."
1.464 +
1.465 + return not self.env.get_args().has_key("editing")
1.466 +
1.467 + def handle_request(self):
1.468
1.469 """
1.470 - Show datetime-related controls if already active or if an object needs
1.471 - them for the given 'period'. The given 'index' is used to parameterise
1.472 - individual controls for dynamic manipulation.
1.473 + Handle actions involving the current object, returning an error if one
1.474 + occurred, or None if the request was successfully handled.
1.475 """
1.476
1.477 - p = form_period_from_period(period)
1.478 + # Handle a submitted form.
1.479
1.480 - page = self.page
1.481 args = self.env.get_args()
1.482 - _id = self.element_identifier
1.483 - _name = self.element_name
1.484 - _enable = self.element_enable
1.485 +
1.486 + # Get the possible actions.
1.487 +
1.488 + reply = args.has_key("reply")
1.489 + discard = args.has_key("discard")
1.490 + create = args.has_key("create")
1.491 + cancel = args.has_key("cancel")
1.492 + ignore = args.has_key("ignore")
1.493 + save = args.has_key("save")
1.494
1.495 - # Add a dynamic stylesheet to permit the controls to modify the display.
1.496 - # NOTE: The style details need to be coordinated with the static
1.497 - # NOTE: stylesheet.
1.498 + have_action = reply or discard or create or cancel or ignore or save
1.499 +
1.500 + if not have_action:
1.501 + return ["action"]
1.502
1.503 - if index is not None:
1.504 - page.style(type="text/css")
1.505 + # If ignoring the object, return to the calendar.
1.506
1.507 - # Unlike the rules for object properties, these affect recurrence
1.508 - # properties.
1.509 + if ignore:
1.510 + self.redirect(self.env.get_path())
1.511 + return None
1.512 +
1.513 + # Update the object.
1.514
1.515 - page.add("""\
1.516 -input#dttimes-enable-%(index)d,
1.517 -input#dtend-enable-%(index)d,
1.518 -input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
1.519 -input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
1.520 -input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
1.521 -input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
1.522 - display: none;
1.523 -}""" % {"index" : index})
1.524 + single_user = False
1.525 +
1.526 + if reply or create or cancel or save:
1.527 +
1.528 + # Update principal event details if organiser.
1.529 +
1.530 + if self.is_organiser():
1.531 +
1.532 + # Update time periods (main and recurring).
1.533
1.534 - page.style.close()
1.535 + try:
1.536 + period = self.handle_main_period()
1.537 + except PeriodError, exc:
1.538 + return exc.args
1.539
1.540 - self.control(
1.541 - _name("dtend-control", "recur", index), "checkbox",
1.542 - _enable(index), p.end_enabled,
1.543 - id=_id("dtend-enable", index)
1.544 - )
1.545 + try:
1.546 + periods = self.handle_recurrence_periods()
1.547 + except PeriodError, exc:
1.548 + return exc.args
1.549 +
1.550 + # Set the periods in the object, first obtaining removed and
1.551 + # modified period information.
1.552 +
1.553 + to_unschedule = self.get_removed_periods()
1.554 +
1.555 + self.obj.set_period(period)
1.556 + self.obj.set_periods(periods)
1.557
1.558 - self.control(
1.559 - _name("dttimes-control", "recur", index), "checkbox",
1.560 - _enable(index), p.times_enabled,
1.561 - id=_id("dttimes-enable", index)
1.562 - )
1.563 + # Update summary.
1.564 +
1.565 + if args.has_key("summary"):
1.566 + self.obj["SUMMARY"] = [(args["summary"][0], {})]
1.567 +
1.568 + # Obtain any participants and those to be removed.
1.569
1.570 - def show_datetime_controls(self, formdate, show_start):
1.571 + attendees = self.get_attendees_from_page()
1.572 + removed = [attendees[int(i)] for i in args.get("remove", [])]
1.573 + to_cancel = self.update_attendees(self.obj, attendees, removed)
1.574 + single_user = not attendees or attendees == [self.user]
1.575 +
1.576 + # Update attendee participation for the current user.
1.577
1.578 - """
1.579 - Show datetime details from the current object for the 'formdate',
1.580 - showing start details if 'show_start' is set to a true value. Details
1.581 - will appear as controls for organisers and labels for attendees.
1.582 - """
1.583 + if args.has_key("partstat"):
1.584 + self.update_participation(self.obj, args["partstat"][0])
1.585 +
1.586 + # Process any action.
1.587
1.588 - page = self.page
1.589 + invite = not save and create and not single_user
1.590 + save = save or create and single_user
1.591
1.592 - # Show controls for editing as organiser.
1.593 + handled = True
1.594
1.595 - if self.is_organiser():
1.596 - page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
1.597 + if reply or invite or cancel:
1.598 +
1.599 + client = ManagerClient(self.obj, self.user, self.messenger)
1.600
1.601 - if show_start:
1.602 - page.div(class_="dt enabled")
1.603 - self.date_controls("dtstart", formdate)
1.604 - page.br()
1.605 - page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
1.606 - page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
1.607 - page.div.close()
1.608 + # Process the object and remove it from the list of requests.
1.609 +
1.610 + if reply and client.process_received_request():
1.611 + self.remove_request(self.uid, self.recurrenceid)
1.612 +
1.613 + elif self.is_organiser() and (invite or cancel):
1.614 +
1.615 + # Invitation, uninvitation and unscheduling...
1.616 +
1.617 + if client.process_created_request(
1.618 + invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule):
1.619 +
1.620 + self.remove_request(self.uid, self.recurrenceid)
1.621
1.622 - else:
1.623 - page.div(class_="dt disabled")
1.624 - page.label("Specify end date", for_="dtend-enable", class_="enable")
1.625 - page.div.close()
1.626 - page.div(class_="dt enabled")
1.627 - self.date_controls("dtend", formdate)
1.628 - page.br()
1.629 - page.label("End on same day", for_="dtend-enable", class_="disable")
1.630 - page.div.close()
1.631 + # Save single user events.
1.632 +
1.633 + elif save:
1.634 + self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node())
1.635 + self.update_event_in_freebusy()
1.636 + self.remove_request(self.uid, self.recurrenceid)
1.637
1.638 - page.td.close()
1.639 + # Remove the request and the object.
1.640
1.641 - # Show a label as attendee.
1.642 + elif discard:
1.643 + self.remove_event_from_freebusy()
1.644 + self.remove_event(self.uid, self.recurrenceid)
1.645 + self.remove_request(self.uid, self.recurrenceid)
1.646
1.647 else:
1.648 - dt = formdate.as_datetime()
1.649 - if dt:
1.650 - page.td(self.format_datetime(dt, "full"))
1.651 - else:
1.652 - page.td("(Unrecognised date)")
1.653 + handled = False
1.654 +
1.655 + # Upon handling an action, redirect to the main page.
1.656 +
1.657 + if handled:
1.658 + self.redirect(self.env.get_path())
1.659 +
1.660 + return None
1.661 +
1.662 + def handle_main_period(self):
1.663 +
1.664 + "Return period details for the main start/end period in an event."
1.665 +
1.666 + return self.get_main_period_from_page().as_event_period()
1.667 +
1.668 + def handle_recurrence_periods(self):
1.669 +
1.670 + "Return period details for the recurrences specified for an event."
1.671 +
1.672 + return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())]
1.673 +
1.674 + # Access to form-originating object information.
1.675 +
1.676 + def get_main_period_from_page(self):
1.677 +
1.678 + "Return the main period defined in the event form."
1.679 +
1.680 + args = self.env.get_args()
1.681 +
1.682 + dtend_enabled = args.get("dtend-control", [None])[0]
1.683 + dttimes_enabled = args.get("dttimes-control", [None])[0]
1.684 + start = self.get_date_control_values("dtstart")
1.685 + end = self.get_date_control_values("dtend")
1.686 +
1.687 + return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid())
1.688
1.689 - def show_recurrence_controls(self, index, period, recurrenceid, recurrenceids, show_start):
1.690 + def get_recurrences_from_page(self):
1.691 +
1.692 + "Return the recurrences defined in the event form."
1.693 +
1.694 + args = self.env.get_args()
1.695 +
1.696 + all_dtend_enabled = args.get("dtend-control-recur", [])
1.697 + all_dttimes_enabled = args.get("dttimes-control-recur", [])
1.698 + all_starts = self.get_date_control_values("dtstart-recur", multiple=True)
1.699 + all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")
1.700 + all_origins = args.get("recur-origin", [])
1.701 +
1.702 + periods = []
1.703 +
1.704 + for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \
1.705 + enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)):
1.706 +
1.707 + dtend_enabled = str(index) in all_dtend_enabled
1.708 + dttimes_enabled = str(index) in all_dttimes_enabled
1.709 + period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin)
1.710 + periods.append(period)
1.711 +
1.712 + return periods
1.713 +
1.714 + def get_removed_periods(self):
1.715 +
1.716 + "Return a list of recurrence periods to remove upon updating an event."
1.717 +
1.718 + to_unschedule = []
1.719 + args = self.env.get_args()
1.720 + for i in args.get("recur-remove", []):
1.721 + to_unschedule.append(periods[int(i)])
1.722 + return to_unschedule
1.723 +
1.724 + def get_attendees_from_page(self):
1.725
1.726 """
1.727 - Show datetime details from the current object for the recurrence having
1.728 - the given 'index', with the recurrence period described by 'period',
1.729 - indicating a start, end and origin of the period from the event details,
1.730 - employing any 'recurrenceid' and 'recurrenceids' for the object to
1.731 - configure the displayed information.
1.732 -
1.733 - If 'show_start' is set to a true value, the start details will be shown;
1.734 - otherwise, the end details will be shown.
1.735 + Return attendees from the request, normalised for iCalendar purposes,
1.736 + and without duplicates.
1.737 """
1.738
1.739 - page = self.page
1.740 - _id = self.element_identifier
1.741 - _name = self.element_name
1.742 + args = self.env.get_args()
1.743
1.744 - p = event_period_from_period(period)
1.745 - replaced = not recurrenceid and p.is_replaced(recurrenceids)
1.746 -
1.747 - # Show controls for editing as organiser.
1.748 + attendees = args.get("attendee", [])
1.749 + unique_attendees = set()
1.750 + ordered_attendees = []
1.751
1.752 - if self.is_organiser() and not replaced:
1.753 - page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
1.754 -
1.755 - read_only = period.origin == "RRULE"
1.756 + for attendee in attendees:
1.757 + if not attendee.strip():
1.758 + continue
1.759 + attendee = get_uri(attendee)
1.760 + if attendee not in unique_attendees:
1.761 + unique_attendees.add(attendee)
1.762 + ordered_attendees.append(attendee)
1.763
1.764 - if show_start:
1.765 - page.div(class_="dt enabled")
1.766 - self.date_controls(_name("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only)
1.767 - if not read_only:
1.768 - page.br()
1.769 - page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable")
1.770 - page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable")
1.771 - page.div.close()
1.772 + return ordered_attendees
1.773 +
1.774 + def update_attendees_from_page(self):
1.775 +
1.776 + "Add or remove attendees. This does not affect the stored object."
1.777 +
1.778 + args = self.env.get_args()
1.779 +
1.780 + attendees = self.get_attendees_from_page()
1.781
1.782 - # Put the origin somewhere.
1.783 + if args.has_key("add"):
1.784 + attendees.append("")
1.785
1.786 - self.control("recur-origin", "hidden", p.origin or "")
1.787 + # Only actually remove attendees if the event is unsent, if the attendee
1.788 + # is new, or if it is the current user being removed.
1.789 +
1.790 + if args.has_key("remove"):
1.791 + still_to_remove = []
1.792
1.793 - else:
1.794 - page.div(class_="dt disabled")
1.795 - if not read_only:
1.796 - page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable")
1.797 - page.div.close()
1.798 - page.div(class_="dt enabled")
1.799 - self.date_controls(_name("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only)
1.800 - if not read_only:
1.801 - page.br()
1.802 - page.label("End on same day", for_=_id("dtend-enable", index), class_="disable")
1.803 - page.div.close()
1.804 + for i in args["remove"]:
1.805 + try:
1.806 + attendee = attendees[int(i)]
1.807 + except IndexError:
1.808 + continue
1.809
1.810 - page.td.close()
1.811 -
1.812 - # Show label as attendee.
1.813 + if self.can_remove_attendee(attendee):
1.814 + attendees.remove(attendee)
1.815 + else:
1.816 + still_to_remove.append(i)
1.817
1.818 - else:
1.819 - self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start)
1.820 + args["remove"] = still_to_remove
1.821 +
1.822 + return attendees
1.823
1.824 - def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start):
1.825 + # Access to current object information.
1.826 +
1.827 + def get_current_main_period(self):
1.828
1.829 """
1.830 - Show datetime details for the given 'period', employing any
1.831 - 'recurrenceid' and 'recurrenceids' for the object to configure the
1.832 - displayed information.
1.833 + Return the currently active main period for the current object depending
1.834 + on whether editing has begun or whether the object has just been loaded.
1.835 + """
1.836
1.837 - If 'show_start' is set to a true value, the start details will be shown;
1.838 - otherwise, the end details will be shown.
1.839 + if self.is_initial_load() or not self.is_organiser():
1.840 + return self.get_stored_main_period()
1.841 + else:
1.842 + return self.get_main_period_from_page()
1.843 +
1.844 + def get_current_recurrences(self):
1.845 +
1.846 + """
1.847 + Return recurrences for the current object using the original object
1.848 + details where no editing is in progress, using form data otherwise.
1.849 """
1.850
1.851 - page = self.page
1.852 + if self.is_initial_load() or not self.is_organiser():
1.853 + return self.get_stored_recurrences()
1.854 + else:
1.855 + return self.get_recurrences_from_page()
1.856
1.857 - p = event_period_from_period(period)
1.858 - replaced = not recurrenceid and p.is_replaced(recurrenceids)
1.859 + def get_current_attendees(self):
1.860 +
1.861 + """
1.862 + Return attendees for the current object depending on whether the object
1.863 + has been edited or instead provides such information from its stored
1.864 + form.
1.865 + """
1.866
1.867 - css = " ".join([
1.868 - replaced and "replaced" or "",
1.869 - p.is_affected(recurrenceid) and "affected" or ""
1.870 - ])
1.871 + if self.is_initial_load() or not self.is_organiser():
1.872 + return self.get_stored_attendees()
1.873 + else:
1.874 + return self.get_attendees_from_page()
1.875 +
1.876 + def update_current_attendees(self):
1.877
1.878 - formdate = show_start and p.get_form_start() or p.get_form_end()
1.879 - dt = formdate.as_datetime()
1.880 - if dt:
1.881 - page.td(self.format_datetime(dt, "long"), class_=css)
1.882 + "Return an updated collection of attendees for the current object."
1.883 +
1.884 + if self.is_initial_load() or not self.is_organiser():
1.885 + return self.get_stored_attendees()
1.886 else:
1.887 - page.td("(Unrecognised date)")
1.888 + return self.update_attendees_from_page()
1.889
1.890 # Full page output methods.
1.891
2.1 --- a/imipweb/resource.py Thu Sep 24 20:41:58 2015 +0200
2.2 +++ b/imipweb/resource.py Thu Sep 24 23:41:47 2015 +0200
2.3 @@ -24,6 +24,7 @@
2.4 from imiptools.data import get_uri, uri_values
2.5 from imiptools.dates import format_datetime, get_recurrence_start_point, to_date
2.6 from imiptools.period import remove_period, remove_affected_period
2.7 +from imipweb.data import event_period_from_period, form_period_from_period, FormDate
2.8 from imipweb.env import CGIEnvironment
2.9 import babel.dates
2.10 import imip_store
2.11 @@ -215,7 +216,7 @@
2.12
2.13 class FormUtilities:
2.14
2.15 - "Utility methods."
2.16 + "Utility methods resource mix-in."
2.17
2.18 def control(self, name, type, value, selected=False, **kw):
2.19
2.20 @@ -341,4 +342,227 @@
2.21 entries = [(tzid, tzid) for tzid in pytz.all_timezones]
2.22 self.menu(name, default, entries, index=index)
2.23
2.24 +class DateTimeFormUtilities:
2.25 +
2.26 + "Date/time control methods resource mix-in."
2.27 +
2.28 + def show_object_datetime_controls(self, period, index=None):
2.29 +
2.30 + """
2.31 + Show datetime-related controls if already active or if an object needs
2.32 + them for the given 'period'. The given 'index' is used to parameterise
2.33 + individual controls for dynamic manipulation.
2.34 + """
2.35 +
2.36 + p = form_period_from_period(period)
2.37 +
2.38 + page = self.page
2.39 + args = self.env.get_args()
2.40 + _id = self.element_identifier
2.41 + _name = self.element_name
2.42 + _enable = self.element_enable
2.43 +
2.44 + # Add a dynamic stylesheet to permit the controls to modify the display.
2.45 + # NOTE: The style details need to be coordinated with the static
2.46 + # NOTE: stylesheet.
2.47 +
2.48 + if index is not None:
2.49 + page.style(type="text/css")
2.50 +
2.51 + # Unlike the rules for object properties, these affect recurrence
2.52 + # properties.
2.53 +
2.54 + page.add("""\
2.55 +input#dttimes-enable-%(index)d,
2.56 +input#dtend-enable-%(index)d,
2.57 +input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
2.58 +input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
2.59 +input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
2.60 +input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
2.61 + display: none;
2.62 +}""" % {"index" : index})
2.63 +
2.64 + page.style.close()
2.65 +
2.66 + self.control(
2.67 + _name("dtend-control", "recur", index), "checkbox",
2.68 + _enable(index), p.end_enabled,
2.69 + id=_id("dtend-enable", index)
2.70 + )
2.71 +
2.72 + self.control(
2.73 + _name("dttimes-control", "recur", index), "checkbox",
2.74 + _enable(index), p.times_enabled,
2.75 + id=_id("dttimes-enable", index)
2.76 + )
2.77 +
2.78 + def show_datetime_controls(self, formdate, show_start):
2.79 +
2.80 + """
2.81 + Show datetime details from the current object for the 'formdate',
2.82 + showing start details if 'show_start' is set to a true value. Details
2.83 + will appear as controls for organisers and labels for attendees.
2.84 + """
2.85 +
2.86 + page = self.page
2.87 +
2.88 + # Show controls for editing as organiser.
2.89 +
2.90 + if self.is_organiser():
2.91 + page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
2.92 +
2.93 + if show_start:
2.94 + page.div(class_="dt enabled")
2.95 + self.date_controls("dtstart", formdate)
2.96 + page.br()
2.97 + page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
2.98 + page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
2.99 + page.div.close()
2.100 +
2.101 + else:
2.102 + page.div(class_="dt disabled")
2.103 + page.label("Specify end date", for_="dtend-enable", class_="enable")
2.104 + page.div.close()
2.105 + page.div(class_="dt enabled")
2.106 + self.date_controls("dtend", formdate)
2.107 + page.br()
2.108 + page.label("End on same day", for_="dtend-enable", class_="disable")
2.109 + page.div.close()
2.110 +
2.111 + page.td.close()
2.112 +
2.113 + # Show a label as attendee.
2.114 +
2.115 + else:
2.116 + dt = formdate.as_datetime()
2.117 + if dt:
2.118 + page.td(self.format_datetime(dt, "full"))
2.119 + else:
2.120 + page.td("(Unrecognised date)")
2.121 +
2.122 + def show_recurrence_controls(self, index, period, recurrenceid, recurrenceids, show_start):
2.123 +
2.124 + """
2.125 + Show datetime details from the current object for the recurrence having
2.126 + the given 'index', with the recurrence period described by 'period',
2.127 + indicating a start, end and origin of the period from the event details,
2.128 + employing any 'recurrenceid' and 'recurrenceids' for the object to
2.129 + configure the displayed information.
2.130 +
2.131 + If 'show_start' is set to a true value, the start details will be shown;
2.132 + otherwise, the end details will be shown.
2.133 + """
2.134 +
2.135 + page = self.page
2.136 + _id = self.element_identifier
2.137 + _name = self.element_name
2.138 +
2.139 + p = event_period_from_period(period)
2.140 + replaced = not recurrenceid and p.is_replaced(recurrenceids)
2.141 +
2.142 + # Show controls for editing as organiser.
2.143 +
2.144 + if self.is_organiser() and not replaced:
2.145 + page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
2.146 +
2.147 + read_only = period.origin == "RRULE"
2.148 +
2.149 + if show_start:
2.150 + page.div(class_="dt enabled")
2.151 + self.date_controls(_name("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only)
2.152 + if not read_only:
2.153 + page.br()
2.154 + page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable")
2.155 + page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable")
2.156 + page.div.close()
2.157 +
2.158 + # Put the origin somewhere.
2.159 +
2.160 + self.control("recur-origin", "hidden", p.origin or "")
2.161 +
2.162 + else:
2.163 + page.div(class_="dt disabled")
2.164 + if not read_only:
2.165 + page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable")
2.166 + page.div.close()
2.167 + page.div(class_="dt enabled")
2.168 + self.date_controls(_name("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only)
2.169 + if not read_only:
2.170 + page.br()
2.171 + page.label("End on same day", for_=_id("dtend-enable", index), class_="disable")
2.172 + page.div.close()
2.173 +
2.174 + page.td.close()
2.175 +
2.176 + # Show label as attendee.
2.177 +
2.178 + else:
2.179 + self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start)
2.180 +
2.181 + def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start):
2.182 +
2.183 + """
2.184 + Show datetime details for the given 'period', employing any
2.185 + 'recurrenceid' and 'recurrenceids' for the object to configure the
2.186 + displayed information.
2.187 +
2.188 + If 'show_start' is set to a true value, the start details will be shown;
2.189 + otherwise, the end details will be shown.
2.190 + """
2.191 +
2.192 + page = self.page
2.193 +
2.194 + p = event_period_from_period(period)
2.195 + replaced = not recurrenceid and p.is_replaced(recurrenceids)
2.196 +
2.197 + css = " ".join([
2.198 + replaced and "replaced" or "",
2.199 + p.is_affected(recurrenceid) and "affected" or ""
2.200 + ])
2.201 +
2.202 + formdate = show_start and p.get_form_start() or p.get_form_end()
2.203 + dt = formdate.as_datetime()
2.204 + if dt:
2.205 + page.td(self.format_datetime(dt, "long"), class_=css)
2.206 + else:
2.207 + page.td("(Unrecognised date)")
2.208 +
2.209 + def get_date_control_values(self, name, multiple=False, tzid_name=None):
2.210 +
2.211 + """
2.212 + Return a dictionary containing date, time and tzid entries for fields
2.213 + starting with 'name'. If 'multiple' is set to a true value, many
2.214 + dictionaries will be returned corresponding to a collection of
2.215 + datetimes. If 'tzid_name' is specified, the time zone information will
2.216 + be acquired from a field starting with 'tzid_name' instead of 'name'.
2.217 + """
2.218 +
2.219 + args = self.env.get_args()
2.220 +
2.221 + dates = args.get("%s-date" % name, [])
2.222 + hours = args.get("%s-hour" % name, [])
2.223 + minutes = args.get("%s-minute" % name, [])
2.224 + seconds = args.get("%s-second" % name, [])
2.225 + tzids = args.get("%s-tzid" % (tzid_name or name), [])
2.226 +
2.227 + # Handle absent values by employing None values.
2.228 +
2.229 + field_values = map(None, dates, hours, minutes, seconds, tzids)
2.230 +
2.231 + if not field_values and not multiple:
2.232 + all_values = FormDate()
2.233 + else:
2.234 + all_values = []
2.235 + for date, hour, minute, second, tzid in field_values:
2.236 + value = FormDate(date, hour, minute, second, tzid or self.get_tzid())
2.237 +
2.238 + # Return a single value or append to a collection of all values.
2.239 +
2.240 + if not multiple:
2.241 + return value
2.242 + else:
2.243 + all_values.append(value)
2.244 +
2.245 + return all_values
2.246 +
2.247 # vim: tabstop=4 expandtab shiftwidth=4