1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator event filtering functionality. 4 5 @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from DateSupport import DateTime, Timespan, TimespanCollection, \ 10 getCurrentDate, getCurrentMonth, cmp_dates_as_day_start 11 12 try: 13 set 14 except NameError: 15 from sets import Set as set 16 17 # Sortable values representing start and end limits of timespans/events. 18 19 START, END = 0, 1 20 21 # Event filtering and limits. 22 23 def getEventsInPeriod(events, calendar_period): 24 25 """ 26 Return a collection containing those of the given 'events' which occur 27 within the given 'calendar_period'. 28 """ 29 30 all_shown_events = [] 31 32 for event in events: 33 34 # Test for the suitability of the event. 35 36 if event.as_timespan() is not None: 37 38 # Compare the dates to the requested calendar window, if any. 39 40 if event in calendar_period: 41 all_shown_events.append(event) 42 43 return all_shown_events 44 45 def getEventLimits(events): 46 47 "Return the earliest and latest of the given 'events'." 48 49 earliest = None 50 latest = None 51 52 for event in events: 53 54 # Test for the suitability of the event. 55 56 if event.as_timespan() is not None: 57 ts = event.as_timespan() 58 if earliest is None or ts.start < earliest: 59 earliest = ts.start 60 if latest is None or ts.end > latest: 61 latest = ts.end 62 63 return earliest, latest 64 65 def getLatestEventTimestamp(events): 66 67 """ 68 Return the latest timestamp found from the given 'events'. 69 """ 70 71 latest = None 72 73 for event in events: 74 metadata = event.getMetadata() 75 76 if latest is None or latest < metadata["last-modified"]: 77 latest = metadata["last-modified"] 78 79 return latest 80 81 def getCalendarPeriod(calendar_start, calendar_end): 82 83 """ 84 Return a calendar period for the given 'calendar_start' and 'calendar_end'. 85 These parameters can be given as None. 86 """ 87 88 # Re-order the window, if appropriate. 89 90 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 91 calendar_start, calendar_end = calendar_end, calendar_start 92 93 return Timespan(calendar_start, calendar_end) 94 95 def getConcretePeriod(calendar_start, calendar_end, earliest, latest, resolution): 96 97 """ 98 From the requested 'calendar_start' and 'calendar_end', which may be None, 99 indicating that no restriction is imposed on the period for each of the 100 boundaries, use the 'earliest' and 'latest' event months to define a 101 specific period of interest. 102 """ 103 104 # Define the period as starting with any specified start month or the 105 # earliest event known, ending with any specified end month or the latest 106 # event known. 107 108 first = calendar_start or earliest 109 last = calendar_end or latest 110 111 # If there is no range of months to show, perhaps because there are no 112 # events in the requested period, and there was no start or end month 113 # specified, show only the month indicated by the start or end of the 114 # requested period. If all events were to be shown but none were found show 115 # the current month. 116 117 if resolution == "date": 118 get_current = getCurrentDate 119 else: 120 get_current = getCurrentMonth 121 122 if first is None: 123 first = last or get_current() 124 if last is None: 125 last = first or get_current() 126 127 if resolution == "month": 128 first = first.as_month() 129 last = last.as_month() 130 131 # Permit "expiring" periods (where the start date approaches the end date). 132 133 return min(first, last), last 134 135 def getCoverage(events, resolution="date"): 136 137 """ 138 Determine the coverage of the given 'events', returning a collection of 139 timespans, along with a dictionary mapping locations to collections of 140 slots, where each slot contains a tuple of the form (timespans, events). 141 """ 142 143 all_events = {} 144 full_coverage = TimespanCollection(resolution) 145 146 # Get event details. 147 148 for event in events: 149 event_details = event.getDetails() 150 151 # Find the coverage of this period for the event. 152 153 # For day views, each location has its own slot, but for month 154 # views, all locations are pooled together since having separate 155 # slots for each location can lead to poor usage of vertical space. 156 157 if resolution == "datetime": 158 event_location = event_details.get("location") 159 else: 160 event_location = None 161 162 # Update the overall coverage. 163 164 full_coverage.insert_in_order(event) 165 166 # Add a new events list for a new location. 167 # Locations can be unspecified, thus None refers to all unlocalised 168 # events. 169 170 if not all_events.has_key(event_location): 171 all_events[event_location] = [TimespanCollection(resolution, [event])] 172 173 # Try and fit the event into an events list. 174 175 else: 176 slot = all_events[event_location] 177 178 for slot_events in slot: 179 180 # Where the event does not overlap with the events in the 181 # current collection, add it alongside these events. 182 183 if not event in slot_events: 184 slot_events.insert_in_order(event) 185 break 186 187 # Make a new element in the list if the event cannot be 188 # marked alongside existing events. 189 190 else: 191 slot.append(TimespanCollection(resolution, [event])) 192 193 return full_coverage, all_events 194 195 def getCoverageScale(coverage): 196 197 """ 198 Return a scale for the given coverage so that the times involved are 199 exposed. The scale consists of a list of non-overlapping timespans forming 200 a contiguous period of time, where each timespan is accompanied in a tuple 201 by a limit and a list of original time details. Thus, the scale consists of 202 (timespan, limit, set-of-times) tuples. 203 """ 204 205 times = {} 206 207 for timespan in coverage: 208 start, end = timespan.as_limits() 209 210 # Add either genuine times or dates converted to times. 211 212 if isinstance(start, DateTime): 213 value = start 214 key = value.to_utc(), START 215 else: 216 value = start.as_start_of_day() 217 key = value, START 218 219 if not times.has_key(key): 220 times[key] = set() 221 times[key].add(value) 222 223 if isinstance(end, DateTime): 224 value = end 225 key = value.to_utc(), END 226 else: 227 value = end.as_date().next_day() 228 key = value, END 229 230 if not times.has_key(key): 231 times[key] = set() 232 times[key].add(value) 233 234 keys = times.keys() 235 keys.sort(cmp_tuples_with_dates_as_day_start) 236 237 scale = [] 238 first = 1 239 start, start_limit = None, None 240 241 for time, limit in keys: 242 if not first: 243 scale.append((Timespan(start, time), limit, times[(start, start_limit)])) 244 else: 245 first = 0 246 start, start_limit = time, limit 247 248 return scale 249 250 def cmp_tuples_with_dates_as_day_start(a, b): 251 252 """ 253 Compare (datetime, limit) tuples, where identical datetimes are 254 distinguished by the limit associated with them. 255 """ 256 257 a_date, a_limit = a 258 b_date, b_limit = b 259 result = cmp_dates_as_day_start(a_date, b_date) 260 261 if result == 0: 262 if a_limit < b_limit: 263 return -1 264 else: 265 return 1 266 267 return result 268 269 # vim: tabstop=4 expandtab shiftwidth=4