1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator library 4 5 @copyright: 2008, 2009, 2010, 2011 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 7 2005-2008 MoinMoin:ThomasWaldmann. 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from MoinMoin.Page import Page 12 from MoinMoin import search, version 13 from MoinMoin import wikiutil 14 import calendar 15 import datetime 16 import time 17 import re 18 import bisect 19 import operator 20 21 try: 22 set 23 except NameError: 24 from sets import Set as set 25 26 try: 27 import pytz 28 except ImportError: 29 pytz = None 30 31 escape = wikiutil.escape 32 33 __version__ = "0.7" 34 35 # Date labels. 36 37 month_labels = ["January", "February", "March", "April", "May", "June", 38 "July", "August", "September", "October", "November", "December"] 39 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 40 41 # Regular expressions where MoinMoin does not provide the required support. 42 43 category_regexp = None 44 45 # Page parsing. 46 47 definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?):: )(?P<desc>.*?)$', re.UNICODE | re.MULTILINE) 48 category_membership_regexp = re.compile(ur"^\s*(?:(Category\S+)(?:\s+(Category\S+))*)\s*$", re.MULTILINE | re.UNICODE) 49 50 # Value parsing. 51 52 country_code_regexp = re.compile(ur'(?:^|\W)(?P<code>[A-Z]{2})(?:$|\W+$)', re.UNICODE) 53 54 month_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})' 55 date_regexp_str = ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})' 56 time_regexp_str = ur'(?P<hour>[0-2][0-9]):(?P<minute>[0-5][0-9])(?::(?P<second>[0-6][0-9]))?' 57 timezone_offset_str = ur'(?P<offset>(UTC)?(?:(?P<sign>[-+])(?P<hours>[0-9]{2})(?::?(?P<minutes>[0-9]{2}))?))' 58 timezone_olson_str = ur'(?P<olson>[a-zA-Z]+(?:/[-_a-zA-Z]+){1,2})' 59 timezone_utc_str = ur'UTC' 60 timezone_regexp_str = ur'(?P<zone>' + timezone_offset_str + '|' + timezone_olson_str + '|' + timezone_utc_str + ')' 61 datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?' 62 63 month_regexp = re.compile(month_regexp_str, re.UNICODE) 64 date_regexp = re.compile(date_regexp_str, re.UNICODE) 65 time_regexp = re.compile(time_regexp_str, re.UNICODE) 66 datetime_regexp = re.compile(datetime_regexp_str, re.UNICODE) 67 timezone_olson_regexp = re.compile(timezone_olson_str, re.UNICODE) 68 timezone_offset_regexp = re.compile(timezone_offset_str, re.UNICODE) 69 70 verbatim_regexp = re.compile(ur'(?:' 71 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 72 ur'|' 73 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 74 ur'|' 75 ur'`(?P<monospace>.*?)`' 76 ur'|' 77 ur'{{{(?P<preformatted>.*?)}}}' 78 ur')', re.UNICODE) 79 80 # Utility functions. 81 82 def getCategoryPattern(request): 83 global category_regexp 84 85 try: 86 return request.cfg.cache.page_category_regexact 87 except AttributeError: 88 89 # Use regular expression from MoinMoin 1.7.1 otherwise. 90 91 if category_regexp is None: 92 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 93 return category_regexp 94 95 def int_or_none(x): 96 if x is None: 97 return x 98 else: 99 return int(x) 100 101 def sort_none_first(x, y): 102 if x is None: 103 return -1 104 elif y is None: 105 return 1 106 else: 107 return cmp(x, y) 108 109 def sign(x): 110 if x < 0: 111 return -1 112 else: 113 return 1 114 115 # Utility classes and associated functions. 116 117 class Form: 118 119 """ 120 A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x 121 environment. 122 """ 123 124 def __init__(self, form): 125 self.form = form 126 127 def get(self, name, default=None): 128 values = self.form.getlist(name) 129 if not values: 130 return default 131 else: 132 return values 133 134 def __getitem__(self, name): 135 return self.form.getlist(name) 136 137 class ActionSupport: 138 139 """ 140 Work around disruptive MoinMoin changes in 1.9, and also provide useful 141 convenience methods. 142 """ 143 144 def get_form(self): 145 return get_form(self.request) 146 147 def _get_selected(self, value, input_value): 148 149 """ 150 Return the HTML attribute text indicating selection of an option (or 151 otherwise) if 'value' matches 'input_value'. 152 """ 153 154 return input_value is not None and value == input_value and 'selected="selected"' or '' 155 156 def _get_selected_for_list(self, value, input_values): 157 158 """ 159 Return the HTML attribute text indicating selection of an option (or 160 otherwise) if 'value' matches one of the 'input_values'. 161 """ 162 163 return value in input_values and 'selected="selected"' or '' 164 165 def _get_input(self, form, name, default=None): 166 167 """ 168 Return the input from 'form' having the given 'name', returning either 169 the input converted to an integer or the given 'default' (optional, None 170 if not specified). 171 """ 172 173 value = form.get(name, [None])[0] 174 if not value: # true if 0 obtained 175 return default 176 else: 177 return int(value) 178 179 def get_month_lists(self, default_as_current=0): 180 181 """ 182 Return two lists of HTML element definitions corresponding to the start 183 and end month selection controls, with months selected according to any 184 values that have been specified via request parameters. 185 """ 186 187 _ = self._ 188 form = self.get_form() 189 190 # Initialise month lists. 191 192 start_month_list = [] 193 end_month_list = [] 194 195 start_month = self._get_input(form, "start-month", default_as_current and getCurrentMonth().month() or None) 196 end_month = self._get_input(form, "end-month", start_month) 197 198 # Prepare month lists, selecting specified months. 199 200 if not default_as_current: 201 start_month_list.append('<option value=""></option>') 202 end_month_list.append('<option value=""></option>') 203 204 for month in range(1, 13): 205 month_label = escape(_(getMonthLabel(month))) 206 selected = self._get_selected(month, start_month) 207 start_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 208 selected = self._get_selected(month, end_month) 209 end_month_list.append('<option value="%02d" %s>%s</option>' % (month, selected, month_label)) 210 211 return start_month_list, end_month_list 212 213 def get_year_defaults(self, default_as_current=0): 214 215 "Return defaults for the start and end years." 216 217 form = self.get_form() 218 219 start_year_default = form.get("start-year", [default_as_current and getCurrentYear() or ""])[0] 220 end_year_default = form.get("end-year", [default_as_current and start_year_default or ""])[0] 221 222 return start_year_default, end_year_default 223 224 def get_day_defaults(self, default_as_current=0): 225 226 "Return defaults for the start and end days." 227 228 form = self.get_form() 229 230 start_day_default = form.get("start-day", [default_as_current and getCurrentDate().day() or ""])[0] 231 end_day_default = form.get("end-day", [default_as_current and start_day_default or ""])[0] 232 233 return start_day_default, end_day_default 234 235 def get_form(request): 236 237 "Work around disruptive MoinMoin changes in 1.9." 238 239 if hasattr(request, "values"): 240 return Form(request.values) 241 else: 242 return request.form 243 244 class send_headers_cls: 245 246 """ 247 A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a 248 1.9.x environment. 249 """ 250 251 def __init__(self, request): 252 self.request = request 253 254 def __call__(self, headers): 255 for header in headers: 256 parts = header.split(":") 257 self.request.headers.add(parts[0], ":".join(parts[1:])) 258 259 def escattr(s): 260 return escape(s, 1) 261 262 # Textual representations. 263 264 def getHTTPTimeString(tmtuple): 265 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( 266 getDayLabel(tmtuple.tm_wday), 267 tmtuple.tm_mday, 268 getMonthLabel(tmtuple.tm_mon), 269 tmtuple.tm_year, 270 tmtuple.tm_hour, 271 tmtuple.tm_min, 272 tmtuple.tm_sec 273 ) 274 275 def getSimpleWikiText(text): 276 277 """ 278 Return the plain text representation of the given 'text' which may employ 279 certain Wiki syntax features, such as those providing verbatim or monospaced 280 text. 281 """ 282 283 # NOTE: Re-implementing support for verbatim text and linking avoidance. 284 285 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 286 287 def getEncodedWikiText(text): 288 289 "Encode the given 'text' in a verbatim representation." 290 291 return "<<Verbatim(%s)>>" % text 292 293 def getPrettyTitle(title): 294 295 "Return a nicely formatted version of the given 'title'." 296 297 return title.replace("_", " ").replace("/", u" ? ") 298 299 def getMonthLabel(month): 300 301 "Return an unlocalised label for the given 'month'." 302 303 return month_labels[month - 1] # zero-based labels 304 305 def getDayLabel(weekday): 306 307 "Return an unlocalised label for the given 'weekday'." 308 309 return weekday_labels[weekday] 310 311 # Action support functions. 312 313 def getPageRevision(page): 314 315 "Return the revision details dictionary for the given 'page'." 316 317 # From Page.edit_info... 318 319 if hasattr(page, "editlog_entry"): 320 line = page.editlog_entry() 321 else: 322 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 323 324 # Similar to Page.mtime_usecs behaviour... 325 326 if line: 327 timestamp = line.ed_time_usecs 328 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 329 comment = line.comment 330 else: 331 mtime = 0 332 comment = "" 333 334 return {"timestamp" : time.gmtime(mtime), "comment" : comment} 335 336 # Category discovery and searching. 337 338 def getCategories(request): 339 340 """ 341 From the AdvancedSearch macro, return a list of category page names using 342 the given 'request'. 343 """ 344 345 # This will return all pages with "Category" in the title. 346 347 cat_filter = getCategoryPattern(request).search 348 return request.rootpage.getPageList(filter=cat_filter) 349 350 def getCategoryMapping(category_pagenames, request): 351 352 """ 353 For the given 'category_pagenames' return a list of tuples of the form 354 (category name, category page name) using the given 'request'. 355 """ 356 357 cat_pattern = getCategoryPattern(request) 358 mapping = [] 359 for pagename in category_pagenames: 360 name = cat_pattern.match(pagename).group("key") 361 if name != "Category": 362 mapping.append((name, pagename)) 363 mapping.sort() 364 return mapping 365 366 def getCategoryPages(pagename, request): 367 368 """ 369 Return the pages associated with the given category 'pagename' using the 370 'request'. 371 """ 372 373 query = search.QueryParser().parse_query('category:%s' % pagename) 374 results = search.searchPages(request, query, "page_name") 375 376 cat_pattern = getCategoryPattern(request) 377 pages = [] 378 for page in results.hits: 379 if not cat_pattern.match(page.page_name): 380 pages.append(page) 381 return pages 382 383 def getAllCategoryPages(category_names, request): 384 385 """ 386 Return all pages belonging to the categories having the given 387 'category_names', using the given 'request'. 388 """ 389 390 pages = [] 391 pagenames = set() 392 393 for category_name in category_names: 394 395 # Get the pages and page names in the category. 396 397 pages_in_category = getCategoryPages(category_name, request) 398 399 # Visit each page in the category. 400 401 for page_in_category in pages_in_category: 402 pagename = page_in_category.page_name 403 404 # Only process each page once. 405 406 if pagename in pagenames: 407 continue 408 else: 409 pagenames.add(pagename) 410 411 pages.append(page_in_category) 412 413 return pages 414 415 def getPagesFromResults(result_pages, request): 416 417 "Return genuine pages for the given 'result_pages' using the 'request'." 418 419 return [Page(request, page.page_name) for page in result_pages] 420 421 # Interfaces. 422 423 class ActsAsTimespan: 424 pass 425 426 # The main activity functions. 427 428 class EventPage: 429 430 "An event page." 431 432 def __init__(self, page): 433 self.page = page 434 self.events = None 435 self.body = None 436 self.categories = None 437 438 def copyPage(self, page): 439 440 "Copy the body of the given 'page'." 441 442 self.body = page.getBody() 443 444 def getPageURL(self, request): 445 446 "Using 'request', return the URL of this page." 447 448 return request.getQualifiedURL(self.page.url(request, relative=0)) 449 450 def getFormat(self): 451 452 "Get the format used on this page." 453 454 return self.page.pi["format"] 455 456 def getRevisions(self): 457 458 "Return a list of page revisions." 459 460 return self.page.getRevList() 461 462 def getPageRevision(self): 463 464 "Return the revision details dictionary for this page." 465 466 return getPageRevision(self.page) 467 468 def getPageName(self): 469 470 "Return the page name." 471 472 return self.page.page_name 473 474 def getPrettyPageName(self): 475 476 "Return a nicely formatted title/name for this page." 477 478 return getPrettyPageName(self.page) 479 480 def getBody(self): 481 482 "Get the current page body." 483 484 if self.body is None: 485 self.body = self.page.get_raw_body() 486 return self.body 487 488 def getEvents(self): 489 490 "Return a list of events from this page." 491 492 if self.events is None: 493 details = {} 494 self.events = [Event(self, details)] 495 496 if self.getFormat() == "wiki": 497 for match in definition_list_regexp.finditer(self.getBody()): 498 499 # Skip commented-out items. 500 501 if match.group("optcomment"): 502 continue 503 504 # Permit case-insensitive list terms. 505 506 term = match.group("term").lower() 507 desc = match.group("desc") 508 509 # Special value type handling. 510 511 # Dates. 512 513 if term in ("start", "end"): 514 desc = getDateTime(desc) 515 516 # Lists (whose elements may be quoted). 517 518 elif term in ("topics", "categories"): 519 desc = [getSimpleWikiText(value.strip()) for value in desc.split(",") if value.strip()] 520 521 # Labels which may well be quoted. 522 523 elif term in ("title", "summary", "description", "location"): 524 desc = getSimpleWikiText(desc.strip()) 525 526 if desc is not None: 527 528 # Handle apparent duplicates by creating a new set of 529 # details. 530 531 if details.has_key(term): 532 533 # Make a new event. 534 535 details = {} 536 self.events.append(Event(self, details)) 537 538 details[term] = desc 539 540 return self.events 541 542 def setEvents(self, events): 543 544 "Set the given 'events' on this page." 545 546 self.events = events 547 548 def getCategoryMembership(self): 549 550 "Get the category names from this page." 551 552 if self.categories is None: 553 body = self.getBody() 554 match = category_membership_regexp.search(body) 555 self.categories = match and [x for x in match.groups() if x] or [] 556 557 return self.categories 558 559 def setCategoryMembership(self, category_names): 560 561 """ 562 Set the category membership for the page using the specified 563 'category_names'. 564 """ 565 566 self.categories = category_names 567 568 def flushEventDetails(self): 569 570 "Flush the current event details to this page's body text." 571 572 new_body_parts = [] 573 end_of_last_match = 0 574 body = self.getBody() 575 576 events = iter(self.getEvents()) 577 578 event = events.next() 579 event_details = event.getDetails() 580 replaced_terms = set() 581 582 for match in definition_list_regexp.finditer(body): 583 584 # Permit case-insensitive list terms. 585 586 term = match.group("term").lower() 587 desc = match.group("desc") 588 589 # Check that the term has not already been substituted. If so, 590 # get the next event. 591 592 if term in replaced_terms: 593 try: 594 event = events.next() 595 596 # No more events. 597 598 except StopIteration: 599 break 600 601 event_details = event.getDetails() 602 replaced_terms = set() 603 604 # Add preceding text to the new body. 605 606 new_body_parts.append(body[end_of_last_match:match.start()]) 607 608 # Get the matching regions, adding the term to the new body. 609 610 new_body_parts.append(match.group("wholeterm")) 611 612 # Special value type handling. 613 614 if event_details.has_key(term): 615 616 # Dates. 617 618 if term in ("start", "end"): 619 desc = desc.replace("YYYY-MM-DD", str(event_details[term])) 620 621 # Lists (whose elements may be quoted). 622 623 elif term in ("topics", "categories"): 624 desc = ", ".join([getEncodedWikiText(item) for item in event_details[term]]) 625 626 # Labels which must be quoted. 627 628 elif term in ("title", "summary"): 629 desc = getEncodedWikiText(event_details[term]) 630 631 # Text which need not be quoted, but it will be Wiki text. 632 633 elif term in ("description", "link", "location"): 634 desc = event_details[term] 635 636 replaced_terms.add(term) 637 638 # Add the replaced value. 639 640 new_body_parts.append(desc) 641 642 # Remember where in the page has been processed. 643 644 end_of_last_match = match.end() 645 646 # Write the rest of the page. 647 648 new_body_parts.append(body[end_of_last_match:]) 649 650 self.body = "".join(new_body_parts) 651 652 def flushCategoryMembership(self): 653 654 "Flush the category membership to the page body." 655 656 body = self.getBody() 657 category_names = self.getCategoryMembership() 658 match = category_membership_regexp.search(body) 659 660 if match: 661 self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]]) 662 663 def saveChanges(self): 664 665 "Save changes to the event." 666 667 self.flushEventDetails() 668 self.flushCategoryMembership() 669 self.page.saveText(self.getBody(), 0) 670 671 def linkToPage(self, request, text, query_string=None): 672 673 """ 674 Using 'request', return a link to this page with the given link 'text' 675 and optional 'query_string'. 676 """ 677 678 return linkToPage(request, self.page, text, query_string) 679 680 class Event(ActsAsTimespan): 681 682 "A description of an event." 683 684 def __init__(self, page, details): 685 self.page = page 686 self.details = details 687 688 def __repr__(self): 689 return "<Event %r %r>" % (self.getSummary(), self.as_limits()) 690 691 def __hash__(self): 692 return hash(self.getSummary()) 693 694 def getPage(self): 695 696 "Return the page describing this event." 697 698 return self.page 699 700 def setPage(self, page): 701 702 "Set the 'page' describing this event." 703 704 self.page = page 705 706 def getSummary(self, event_parent=None): 707 708 """ 709 Return either the given title or summary of the event according to the 710 event details, or a summary made from using the pretty version of the 711 page name. 712 713 If the optional 'event_parent' is specified, any page beneath the given 714 'event_parent' page in the page hierarchy will omit this parent information 715 if its name is used as the summary. 716 """ 717 718 event_details = self.details 719 720 if event_details.has_key("title"): 721 return event_details["title"] 722 elif event_details.has_key("summary"): 723 return event_details["summary"] 724 else: 725 # If appropriate, remove the parent details and "/" character. 726 727 title = self.page.getPageName() 728 729 if event_parent and title.startswith(event_parent): 730 title = title[len(event_parent.rstrip("/")) + 1:] 731 732 return getPrettyTitle(title) 733 734 def getDetails(self): 735 736 "Return the details for this event." 737 738 return self.details 739 740 def setDetails(self, event_details): 741 742 "Set the 'event_details' for this event." 743 744 self.details = event_details 745 746 # Timespan-related methods. 747 748 def __contains__(self, other): 749 return self == other 750 751 def __cmp__(self, other): 752 if isinstance(other, Event): 753 return cmp(self.as_timespan(), other.as_timespan()) 754 else: 755 return cmp(self.as_timespan(), other) 756 757 def as_timespan(self): 758 details = self.details 759 if details.has_key("start") and details.has_key("end"): 760 return Timespan(details["start"], details["end"]) 761 else: 762 return None 763 764 def as_limits(self): 765 ts = self.as_timespan() 766 return ts and ts.as_limits() 767 768 def getEventsFromPages(pages): 769 770 "Return a list of events found on the given 'pages'." 771 772 events = [] 773 774 for page in pages: 775 776 # Get a real page, not a result page. 777 778 event_page = EventPage(page) 779 780 # Get all events described in the page. 781 782 for event in event_page.getEvents(): 783 784 # Remember the event. 785 786 events.append(event) 787 788 return events 789 790 def getEventsInPeriod(events, calendar_period): 791 792 """ 793 Return a collection containing those of the given 'events' which occur 794 within the given 'calendar_period'. 795 """ 796 797 all_shown_events = [] 798 799 for event in events: 800 801 # Test for the suitability of the event. 802 803 if event.as_timespan() is not None: 804 805 # Compare the dates to the requested calendar window, if any. 806 807 if event in calendar_period: 808 all_shown_events.append(event) 809 810 return all_shown_events 811 812 def getEventLimits(events): 813 814 "Return the earliest and latest of the given 'events'." 815 816 earliest = None 817 latest = None 818 819 for event in events: 820 821 # Test for the suitability of the event. 822 823 if event.as_timespan() is not None: 824 ts = event.as_timespan() 825 if earliest is None or ts.start < earliest: 826 earliest = ts.start 827 if latest is None or ts.end > latest: 828 latest = ts.end 829 830 return earliest, latest 831 832 def setEventTimestamps(request, events): 833 834 """ 835 Using 'request', set timestamp details in the details dictionary of each of 836 the 'events'. 837 838 Return the latest timestamp found. 839 """ 840 841 latest = None 842 843 for event in events: 844 event_details = event.getDetails() 845 event_page = event.getPage() 846 847 # Get the initial revision of the page. 848 849 revisions = event_page.getRevisions() 850 event_page_initial = Page(request, event_page.getPageName(), rev=revisions[-1]) 851 852 # Get the created and last modified times. 853 854 initial_revision = getPageRevision(event_page_initial) 855 event_details["created"] = initial_revision["timestamp"] 856 latest_revision = event_page.getPageRevision() 857 event_details["last-modified"] = latest_revision["timestamp"] 858 event_details["sequence"] = len(revisions) - 1 859 event_details["last-comment"] = latest_revision["comment"] 860 861 if latest is None or latest < event_details["last-modified"]: 862 latest = event_details["last-modified"] 863 864 return latest 865 866 def getOrderedEvents(events): 867 868 """ 869 Return a list with the given 'events' ordered according to their start and 870 end dates. 871 """ 872 873 ordered_events = events[:] 874 ordered_events.sort() 875 return ordered_events 876 877 def getCalendarPeriod(calendar_start, calendar_end): 878 879 """ 880 Return a calendar period for the given 'calendar_start' and 'calendar_end'. 881 These parameters can be given as None. 882 """ 883 884 # Re-order the window, if appropriate. 885 886 if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end: 887 calendar_start, calendar_end = calendar_end, calendar_start 888 889 return Timespan(calendar_start, calendar_end) 890 891 def getConcretePeriod(calendar_start, calendar_end, earliest, latest): 892 893 """ 894 From the requested 'calendar_start' and 'calendar_end', which may be None, 895 indicating that no restriction is imposed on the period for each of the 896 boundaries, use the 'earliest' and 'latest' event months to define a 897 specific period of interest. 898 """ 899 900 # Define the period as starting with any specified start month or the 901 # earliest event known, ending with any specified end month or the latest 902 # event known. 903 904 first = calendar_start or earliest 905 last = calendar_end or latest 906 907 # If there is no range of months to show, perhaps because there are no 908 # events in the requested period, and there was no start or end month 909 # specified, show only the month indicated by the start or end of the 910 # requested period. If all events were to be shown but none were found show 911 # the current month. 912 913 if isinstance(first, Date): 914 get_current = getCurrentDate 915 else: 916 get_current = getCurrentMonth 917 918 if first is None: 919 first = last or get_current() 920 if last is None: 921 last = first or get_current() 922 923 # Permit "expiring" periods (where the start date approaches the end date). 924 925 return min(first, last), last 926 927 def getCoverage(events, resolution="date"): 928 929 """ 930 Determine the coverage of the given 'events', returning a collection of 931 timespans, along with a dictionary mapping locations to collections of 932 slots, where each slot contains a tuple of the form (timespans, events). 933 """ 934 935 all_events = {} 936 full_coverage = TimespanCollection(resolution) 937 938 # Get event details. 939 940 for event in events: 941 event_details = event.getDetails() 942 943 # Find the coverage of this period for the event. 944 945 # For day views, each location has its own slot, but for month 946 # views, all locations are pooled together since having separate 947 # slots for each location can lead to poor usage of vertical space. 948 949 if resolution == "datetime": 950 event_location = event_details.get("location") 951 else: 952 event_location = None 953 954 # Update the overall coverage. 955 956 full_coverage.insert_in_order(event) 957 958 # Add a new events list for a new location. 959 # Locations can be unspecified, thus None refers to all unlocalised 960 # events. 961 962 if not all_events.has_key(event_location): 963 all_events[event_location] = [TimespanCollection(resolution, [event])] 964 965 # Try and fit the event into an events list. 966 967 else: 968 slot = all_events[event_location] 969 970 for slot_events in slot: 971 972 # Where the event does not overlap with the events in the 973 # current collection, add it alongside these events. 974 975 if not event in slot_events: 976 slot_events.insert_in_order(event) 977 break 978 979 # Make a new element in the list if the event cannot be 980 # marked alongside existing events. 981 982 else: 983 slot.append(TimespanCollection(resolution, [event])) 984 985 return full_coverage, all_events 986 987 def getCoverageScale(coverage): 988 989 """ 990 Return a scale for the given coverage so that the times involved are 991 exposed. The scale consists of a list of non-overlapping timespans forming 992 a contiguous period of time. 993 """ 994 995 times = set() 996 for timespan in coverage: 997 start, end = timespan.as_limits() 998 999 # Add either genuine times or dates converted to times. 1000 1001 if isinstance(start, DateTime): 1002 times.add(start) 1003 else: 1004 times.add(start.as_datetime(None, None, None, None)) 1005 1006 if isinstance(end, DateTime): 1007 times.add(end) 1008 else: 1009 times.add(end.as_date().next_day()) 1010 1011 times = list(times) 1012 times.sort(cmp_dates_as_day_start) 1013 1014 scale = [] 1015 first = 1 1016 start = None 1017 for time in times: 1018 if not first: 1019 scale.append(Timespan(start, time)) 1020 else: 1021 first = 0 1022 start = time 1023 1024 return scale 1025 1026 # Date-related functions. 1027 1028 def cmp_dates_as_day_start(a, b): 1029 1030 """ 1031 Compare dates/datetimes 'a' and 'b' treating dates without time information 1032 as the earliest time in a particular day. 1033 """ 1034 1035 are_equal = a == b 1036 1037 if are_equal: 1038 a2 = a.as_datetime_or_date() 1039 b2 = b.as_datetime_or_date() 1040 1041 if isinstance(a2, Date) and isinstance(b2, DateTime): 1042 return -1 1043 elif isinstance(a2, DateTime) and isinstance(b2, Date): 1044 return 1 1045 1046 return cmp(a, b) 1047 1048 class Period: 1049 1050 "A simple period of time." 1051 1052 def __init__(self, data): 1053 self.data = data 1054 1055 def months(self): 1056 return self.data[0] * 12 + self.data[1] 1057 1058 class Convertible: 1059 1060 "Support for converting temporal objects." 1061 1062 def _get_converter(self, resolution): 1063 if resolution == "month": 1064 return lambda x: x and x.as_month() 1065 elif resolution == "date": 1066 return lambda x: x and x.as_date() 1067 elif resolution == "datetime": 1068 return lambda x: x and x.as_datetime_or_date() 1069 else: 1070 return lambda x: x 1071 1072 class Temporal(Convertible): 1073 1074 "A simple temporal representation, common to dates and times." 1075 1076 def __init__(self, data): 1077 self.data = list(data) 1078 1079 def __repr__(self): 1080 return "%s(%r)" % (self.__class__.__name__, self.data) 1081 1082 def __hash__(self): 1083 return hash(self.as_tuple()) 1084 1085 def as_tuple(self): 1086 return tuple(self.data) 1087 1088 def convert(self, resolution): 1089 return self._get_converter(resolution)(self) 1090 1091 def __cmp__(self, other): 1092 1093 """ 1094 The result of comparing this instance with 'other' is derived from a 1095 comparison of the instances' date(time) data at the highest common 1096 resolution, meaning that if a date is compared to a datetime, the 1097 datetime will be considered as a date. Thus, a date and a datetime 1098 referring to the same date will be considered equal. 1099 """ 1100 1101 if not isinstance(other, Temporal): 1102 return NotImplemented 1103 else: 1104 data = self.as_tuple() 1105 other_data = other.as_tuple() 1106 length = min(len(data), len(other_data)) 1107 return cmp(data[:length], other_data[:length]) 1108 1109 def until(self, start, end, nextfn, prevfn): 1110 1111 """ 1112 Return a collection of units of time by starting from the given 'start' 1113 and stepping across intervening units until 'end' is reached, using the 1114 given 'nextfn' and 'prevfn' to step from one unit to the next. 1115 """ 1116 1117 current = start 1118 units = [current] 1119 if current < end: 1120 while current < end: 1121 current = nextfn(current) 1122 units.append(current) 1123 elif current > end: 1124 while current > end: 1125 current = prevfn(current) 1126 units.append(current) 1127 return units 1128 1129 def ambiguous(self): 1130 1131 "Only times can be ambiguous." 1132 1133 return 0 1134 1135 class Month(Temporal): 1136 1137 "A simple year-month representation." 1138 1139 def __str__(self): 1140 return "%04d-%02d" % self.as_tuple()[:2] 1141 1142 def as_datetime(self, day, hour, minute, second, zone): 1143 return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) 1144 1145 def as_date(self, day): 1146 return Date(self.as_tuple() + (day,)) 1147 1148 def as_month(self): 1149 return self 1150 1151 def year(self): 1152 return self.data[0] 1153 1154 def month(self): 1155 return self.data[1] 1156 1157 def month_properties(self): 1158 1159 """ 1160 Return the weekday of the 1st of the month, along with the number of 1161 days, as a tuple. 1162 """ 1163 1164 year, month = self.as_tuple()[:2] 1165 return calendar.monthrange(year, month) 1166 1167 def month_update(self, n=1): 1168 1169 "Return the month updated by 'n' months." 1170 1171 year, month = self.as_tuple()[:2] 1172 return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1)) 1173 1174 def next_month(self): 1175 1176 "Return the month following this one." 1177 1178 return self.month_update(1) 1179 1180 def previous_month(self): 1181 1182 "Return the month preceding this one." 1183 1184 return self.month_update(-1) 1185 1186 def __sub__(self, start): 1187 1188 """ 1189 Return the difference in years and months between this month and the 1190 'start' month as a period. 1191 """ 1192 1193 return Period([(x - y) for x, y in zip(self.data, start.data)]) 1194 1195 def months_until(self, end): 1196 1197 "Return the collection of months from this month until 'end'." 1198 1199 return self.until(self.as_month(), end.as_month(), Month.next_month, Month.previous_month) 1200 1201 class Date(Month): 1202 1203 "A simple year-month-day representation." 1204 1205 def constrain(self): 1206 year, month, day = self.as_tuple()[:3] 1207 1208 month = max(min(month, 12), 1) 1209 wd, last_day = calendar.monthrange(year, month) 1210 day = max(min(day, last_day), 1) 1211 1212 self.data[1:3] = month, day 1213 1214 def __str__(self): 1215 return "%04d-%02d-%02d" % self.as_tuple()[:3] 1216 1217 def as_datetime(self, hour, minute, second, zone): 1218 return DateTime(self.as_tuple() + (hour, minute, second, zone)) 1219 1220 def as_date(self): 1221 return self 1222 1223 def as_datetime_or_date(self): 1224 return self 1225 1226 def as_month(self): 1227 return Month(self.data[:2]) 1228 1229 def day(self): 1230 return self.data[2] 1231 1232 def day_update(self, n=1): 1233 1234 "Return the month updated by 'n' days." 1235 1236 delta = datetime.timedelta(n) 1237 dt = datetime.date(*self.as_tuple()[:3]) 1238 dt_new = dt + delta 1239 return Date((dt_new.year, dt_new.month, dt_new.day)) 1240 1241 def next_day(self): 1242 1243 "Return the date following this one." 1244 1245 year, month, day = self.as_tuple()[:3] 1246 _wd, end_day = calendar.monthrange(year, month) 1247 if day == end_day: 1248 if month == 12: 1249 return Date((year + 1, 1, 1)) 1250 else: 1251 return Date((year, month + 1, 1)) 1252 else: 1253 return Date((year, month, day + 1)) 1254 1255 def previous_day(self): 1256 1257 "Return the date preceding this one." 1258 1259 year, month, day = self.as_tuple()[:3] 1260 if day == 1: 1261 if month == 1: 1262 return Date((year - 1, 12, 31)) 1263 else: 1264 _wd, end_day = calendar.monthrange(year, month - 1) 1265 return Date((year, month - 1, end_day)) 1266 else: 1267 return Date((year, month, day - 1)) 1268 1269 def days_until(self, end): 1270 1271 "Return the collection of days from this date until 'end'." 1272 1273 return self.until(self.as_date(), end.as_date(), Date.next_day, Date.previous_day) 1274 1275 class DateTime(Date): 1276 1277 "A simple date plus time representation." 1278 1279 def constrain(self): 1280 Date.constrain(self) 1281 1282 hour, minute, second = self.as_tuple()[3:6] 1283 1284 if self.has_time(): 1285 hour = max(min(hour, 23), 0) 1286 minute = max(min(minute, 59), 0) 1287 1288 if second is not None: 1289 second = max(min(second, 60), 0) # support leap seconds 1290 1291 self.data[3:6] = hour, minute, second 1292 1293 def __str__(self): 1294 return Date.__str__(self) + self.time_string() 1295 1296 def time_string(self): 1297 if self.has_time(): 1298 data = self.as_tuple() 1299 time_str = " %02d:%02d" % data[3:5] 1300 if data[5] is not None: 1301 time_str += ":%02d" % data[5] 1302 if data[6] is not None: 1303 time_str += " %s" % data[6] 1304 return time_str 1305 else: 1306 return "" 1307 1308 def as_datetime(self): 1309 return self 1310 1311 def as_date(self): 1312 return Date(self.data[:3]) 1313 1314 def as_datetime_or_date(self): 1315 1316 """ 1317 Return a date for this datetime if fields are missing. Otherwise, return 1318 this datetime itself. 1319 """ 1320 1321 if not self.has_time(): 1322 return self.as_date() 1323 else: 1324 return self 1325 1326 def __cmp__(self, other): 1327 1328 """ 1329 The result of comparing this instance with 'other' is, if both instances 1330 are datetime instances, derived from a comparison of the datetimes 1331 converted to UTC. If one or both datetimes cannot be converted to UTC, 1332 the datetimes are compared using the basic temporal comparison which 1333 compares their raw time data. 1334 """ 1335 1336 this = self 1337 1338 if this.has_time(): 1339 if isinstance(other, DateTime): 1340 if other.has_time(): 1341 this_utc = this.to_utc() 1342 other_utc = other.to_utc() 1343 if this_utc is not None and other_utc is not None: 1344 return cmp(this_utc.as_tuple(), other_utc.as_tuple()) 1345 else: 1346 other = other.padded() 1347 else: 1348 this = this.padded() 1349 1350 return Date.__cmp__(this, other) 1351 1352 def has_time(self): 1353 1354 """ 1355 Return whether this object has any time information. Objects without 1356 time information can refer to the very start of a day. 1357 """ 1358 1359 return self.data[3] is not None and self.data[4] is not None 1360 1361 def time(self): 1362 return self.data[3:] 1363 1364 def seconds(self): 1365 return self.data[5] 1366 1367 def time_zone(self): 1368 return self.data[6] 1369 1370 def set_time_zone(self, value): 1371 self.data[6] = value 1372 1373 def padded(self, empty_value=0): 1374 1375 """ 1376 Return a datetime with missing fields defined as being the given 1377 'empty_value' or 0 if not specified. 1378 """ 1379 1380 data = [] 1381 for x in self.data[:6]: 1382 if x is None: 1383 data.append(empty_value) 1384 else: 1385 data.append(x) 1386 1387 data += self.data[6:] 1388 return DateTime(data) 1389 1390 def to_utc(self): 1391 1392 """ 1393 Return this object converted to UTC, or None if such a conversion is not 1394 defined. 1395 """ 1396 1397 if not self.has_time(): 1398 return None 1399 1400 offset = self.utc_offset() 1401 if offset: 1402 hours, minutes = offset 1403 1404 # Invert the offset to get the correction. 1405 1406 hours, minutes = -hours, -minutes 1407 1408 # Get the components. 1409 1410 hour, minute, second, zone = self.time() 1411 date = self.as_date() 1412 1413 # Add the minutes and hours. 1414 1415 minute += minutes 1416 if minute < 0 or minute > 59: 1417 hour += minute / 60 1418 minute = minute % 60 1419 1420 # NOTE: This makes various assumptions and probably would not work 1421 # NOTE: for general arithmetic. 1422 1423 hour += hours 1424 if hour < 0: 1425 date = date.previous_day() 1426 hour += 24 1427 elif hour > 23: 1428 date = date.next_day() 1429 hour -= 24 1430 1431 return date.as_datetime(hour, minute, second, "UTC") 1432 1433 # Cannot convert. 1434 1435 else: 1436 return None 1437 1438 def utc_offset(self): 1439 1440 "Return the UTC offset in hours and minutes." 1441 1442 zone = self.time_zone() 1443 if not zone: 1444 return None 1445 1446 # Support explicit UTC zones. 1447 1448 if zone == "UTC": 1449 return 0, 0 1450 1451 # Attempt to return a UTC offset where an explicit offset has been set. 1452 1453 match = timezone_offset_regexp.match(zone) 1454 if match: 1455 if match.group("sign") == "-": 1456 sign = -1 1457 else: 1458 sign = 1 1459 1460 hours = int(match.group("hours")) * sign 1461 minutes = int(match.group("minutes") or 0) * sign 1462 return hours, minutes 1463 1464 # Attempt to handle Olson time zone identifiers. 1465 1466 dt = self.as_olson_datetime() 1467 if dt: 1468 seconds = dt.utcoffset().seconds 1469 hours = seconds / 3600 1470 minutes = (seconds % 3600) / 60 1471 return hours, minutes 1472 1473 # Otherwise return None. 1474 1475 return None 1476 1477 def olson_identifier(self): 1478 1479 "Return the Olson identifier from any zone information." 1480 1481 zone = self.time_zone() 1482 if not zone: 1483 return None 1484 1485 # Attempt to match an identifier. 1486 1487 match = timezone_olson_regexp.match(zone) 1488 if match: 1489 return match.group("olson") 1490 else: 1491 return None 1492 1493 def _as_olson_datetime(self, hours=None): 1494 1495 """ 1496 Return a Python datetime object for this datetime interpreted using any 1497 Olson time zone identifier and the given 'hours' offset, raising one of 1498 the pytz exceptions in case of ambiguity. 1499 """ 1500 1501 olson = self.olson_identifier() 1502 if olson and pytz: 1503 tz = pytz.timezone(olson) 1504 data = self.padded().as_tuple()[:6] 1505 dt = datetime.datetime(*data) 1506 1507 # With an hours offset, find a time probably in a previously 1508 # applicable time zone. 1509 1510 if hours is not None: 1511 td = datetime.timedelta(0, hours * 3600) 1512 dt += td 1513 1514 ldt = tz.localize(dt, None) 1515 1516 # With an hours offset, adjust the time to define it within the 1517 # previously applicable time zone but at the presumably intended 1518 # position. 1519 1520 if hours is not None: 1521 ldt -= td 1522 1523 return ldt 1524 else: 1525 return None 1526 1527 def as_olson_datetime(self): 1528 1529 """ 1530 Return a Python datetime object for this datetime interpreted using any 1531 Olson time zone identifier, choosing the time from the zone before the 1532 period of ambiguity. 1533 """ 1534 1535 try: 1536 return self._as_olson_datetime() 1537 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1538 1539 # Try again, using an earlier local time and then stepping forward 1540 # in the chosen zone. 1541 # NOTE: Four hours earlier seems reasonable. 1542 1543 return self._as_olson_datetime(-4) 1544 1545 def ambiguous(self): 1546 1547 "Return whether the time is local and ambiguous." 1548 1549 try: 1550 self._as_olson_datetime() 1551 except (pytz.UnknownTimeZoneError, pytz.AmbiguousTimeError): 1552 return 1 1553 1554 return 0 1555 1556 class Timespan(ActsAsTimespan, Convertible): 1557 1558 """ 1559 A period of time which can be compared against others to check for overlaps. 1560 """ 1561 1562 def __init__(self, start, end): 1563 self.start = start 1564 self.end = end 1565 1566 # NOTE: Should perhaps catch ambiguous time problems elsewhere. 1567 1568 if self.ambiguous() and self.start is not None and self.end is not None and start > end: 1569 self.start, self.end = end, start 1570 1571 def __repr__(self): 1572 return "%s(%r, %r)" % (self.__class__.__name__, self.start, self.end) 1573 1574 def __hash__(self): 1575 return hash((self.start, self.end)) 1576 1577 def as_timespan(self): 1578 return self 1579 1580 def as_limits(self): 1581 return self.start, self.end 1582 1583 def ambiguous(self): 1584 return self.start is not None and self.start.ambiguous() or self.end is not None and self.end.ambiguous() 1585 1586 def convert(self, resolution): 1587 return Timespan(*map(self._get_converter(resolution), self.as_limits())) 1588 1589 def is_before(self, a, b): 1590 1591 """ 1592 Return whether 'a' is before 'b'. Since the end datetime of one period 1593 may be the same as the start datetime of another period, and yet the 1594 first period is intended to be concluded by the end datetime and not 1595 overlap with the other period, a different test is employed for datetime 1596 comparisons. 1597 """ 1598 1599 # Datetimes without times can be equal to dates and be considered as 1600 # occurring before those dates. Generally, datetimes should not be 1601 # produced without time information as getDateTime converts such 1602 # datetimes to dates. 1603 1604 if isinstance(a, DateTime) and (isinstance(b, DateTime) or not a.has_time()): 1605 return a <= b 1606 else: 1607 return a < b 1608 1609 def __contains__(self, other): 1610 1611 """ 1612 This instance is considered to contain 'other' if one is not before or 1613 after the other. If this instance overlaps or coincides with 'other', 1614 then 'other' is regarded as belonging to this instance's time period. 1615 """ 1616 1617 return self == other 1618 1619 def __cmp__(self, other): 1620 1621 """ 1622 Return whether this timespan occupies the same period of time as the 1623 'other'. Timespans are considered less than others if their end points 1624 precede the other's start point, and are considered greater than others 1625 if their start points follow the other's end point. 1626 """ 1627 1628 if isinstance(other, ActsAsTimespan): 1629 other = other.as_timespan() 1630 1631 if self.end is not None and other.start is not None and self.is_before(self.end, other.start): 1632 return -1 1633 elif self.start is not None and other.end is not None and self.is_before(other.end, self.start): 1634 return 1 1635 else: 1636 return 0 1637 1638 else: 1639 if self.end is not None and self.is_before(self.end, other): 1640 return -1 1641 elif self.start is not None and self.is_before(other, self.start): 1642 return 1 1643 else: 1644 return 0 1645 1646 class TimespanCollection: 1647 1648 """ 1649 A class providing a list-like interface supporting membership tests at a 1650 particular resolution in order to maintain a collection of non-overlapping 1651 timespans. 1652 """ 1653 1654 def __init__(self, resolution, values=None): 1655 self.resolution = resolution 1656 self.values = values or [] 1657 1658 def as_timespan(self): 1659 return Timespan(*self.as_limits()) 1660 1661 def as_limits(self): 1662 1663 "Return the earliest and latest points in time for this collection." 1664 1665 if not self.values: 1666 return None, None 1667 else: 1668 first, last = self.values[0], self.values[-1] 1669 if isinstance(first, ActsAsTimespan): 1670 first = first.as_timespan().start 1671 if isinstance(last, ActsAsTimespan): 1672 last = last.as_timespan().end 1673 return first, last 1674 1675 def convert(self, value): 1676 if isinstance(value, ActsAsTimespan): 1677 ts = value.as_timespan() 1678 return ts and ts.convert(self.resolution) 1679 else: 1680 return value.convert(self.resolution) 1681 1682 def __iter__(self): 1683 return iter(self.values) 1684 1685 def __len__(self): 1686 return len(self.values) 1687 1688 def __getitem__(self, i): 1689 return self.values[i] 1690 1691 def __setitem__(self, i, value): 1692 self.values[i] = value 1693 1694 def __contains__(self, value): 1695 test_value = self.convert(value) 1696 return test_value in self.values 1697 1698 def append(self, value): 1699 self.values.append(value) 1700 1701 def insert(self, i, value): 1702 self.values.insert(i, value) 1703 1704 def pop(self): 1705 return self.values.pop() 1706 1707 def insert_in_order(self, value): 1708 bisect.insort_left(self, value) 1709 1710 def getCountry(s): 1711 1712 "Find a country code in the given string 's'." 1713 1714 match = country_code_regexp.search(s) 1715 1716 if match: 1717 return match.group("code") 1718 else: 1719 return None 1720 1721 def getDate(s): 1722 1723 "Parse the string 's', extracting and returning a date object." 1724 1725 dt = getDateTime(s) 1726 if dt is not None: 1727 return dt.as_date() 1728 else: 1729 return None 1730 1731 def getDateTime(s): 1732 1733 """ 1734 Parse the string 's', extracting and returning a datetime object where time 1735 information has been given or a date object where time information is 1736 absent. 1737 """ 1738 1739 m = datetime_regexp.search(s) 1740 if m: 1741 groups = list(m.groups()) 1742 1743 # Convert date and time data to integer or None. 1744 1745 return DateTime(map(int_or_none, groups[:6]) + [m.group("zone")]).as_datetime_or_date() 1746 else: 1747 return None 1748 1749 def getDateStrings(s): 1750 1751 "Parse the string 's', extracting and returning all date strings." 1752 1753 start = 0 1754 m = date_regexp.search(s, start) 1755 l = [] 1756 while m: 1757 l.append("-".join(m.groups())) 1758 m = date_regexp.search(s, m.end()) 1759 return l 1760 1761 def getMonth(s): 1762 1763 "Parse the string 's', extracting and returning a month object." 1764 1765 m = month_regexp.search(s) 1766 if m: 1767 return Month(map(int, m.groups())) 1768 else: 1769 return None 1770 1771 def getCurrentDate(): 1772 1773 "Return the current date as a (year, month, day) tuple." 1774 1775 today = datetime.date.today() 1776 return Date((today.year, today.month, today.day)) 1777 1778 def getCurrentMonth(): 1779 1780 "Return the current month as a (year, month) tuple." 1781 1782 today = datetime.date.today() 1783 return Month((today.year, today.month)) 1784 1785 def getCurrentYear(): 1786 1787 "Return the current year." 1788 1789 today = datetime.date.today() 1790 return today.year 1791 1792 # Location-related functions. 1793 1794 class Reference: 1795 1796 "A map reference." 1797 1798 def __init__(self, degrees, minutes=0, seconds=0): 1799 self.degrees = degrees 1800 self.minutes = minutes 1801 self.seconds = seconds 1802 1803 def __repr__(self): 1804 return "Reference(%d, %d, %d)" % (self.degrees, self.minutes, self.seconds) 1805 1806 def __add__(self, other): 1807 if not isinstance(other, Reference): 1808 return NotImplemented 1809 else: 1810 s = sign(self.degrees) 1811 o = sign(other.degrees) 1812 carry, seconds = adc(s * self.seconds, o * other.seconds) 1813 carry, minutes = adc(s * self.minutes, o * other.minutes + carry) 1814 return Reference(self.degrees + other.degrees + carry, minutes, seconds) 1815 1816 def __sub__(self, other): 1817 if not isinstance(other, Reference): 1818 return NotImplemented 1819 else: 1820 return self.__add__(Reference(-other.degrees, other.minutes, other.seconds)) 1821 1822 def _compare(self, op, other): 1823 if not isinstance(other, Reference): 1824 return NotImplemented 1825 else: 1826 return op(self.to_degrees(), other.to_degrees()) 1827 1828 def __eq__(self, other): 1829 return self._compare(operator.eq, other) 1830 1831 def __ne__(self, other): 1832 return self._compare(operator.ne, other) 1833 1834 def __lt__(self, other): 1835 return self._compare(operator.lt, other) 1836 1837 def __le__(self, other): 1838 return self._compare(operator.le, other) 1839 1840 def __gt__(self, other): 1841 return self._compare(operator.gt, other) 1842 1843 def __ge__(self, other): 1844 return self._compare(operator.ge, other) 1845 1846 def to_degrees(self): 1847 return sign(self.degrees) * (abs(self.degrees) + self.minutes / 60.0 + self.seconds / 3600.0) 1848 1849 def to_pixels(self, scale): 1850 return self.to_degrees() * scale 1851 1852 def adc(x, y): 1853 result = x + y 1854 return divmod(result, 60) 1855 1856 def getPositionForReference(latitude, longitude, map_y, map_x, map_x_scale, map_y_scale): 1857 return (longitude - map_x).to_pixels(map_x_scale), (latitude - map_y).to_pixels(map_y_scale) 1858 1859 def getPositionForCentrePoint(position, map_x_scale, map_y_scale): 1860 x, y = position 1861 return x - map_x_scale / 2.0, y - map_y_scale / 2.0 1862 1863 def getMapReference(value): 1864 1865 "Return a map reference by parsing the given 'value'." 1866 1867 return Reference(*map(float, value.split(":"))) 1868 1869 # vim: tabstop=4 expandtab shiftwidth=4 1870 1871 # User interface functions. 1872 1873 def getParameter(request, name, default=None): 1874 1875 """ 1876 Using the given 'request', return the value of the parameter with the given 1877 'name', returning the optional 'default' (or None) if no value was supplied 1878 in the 'request'. 1879 """ 1880 1881 return get_form(request).get(name, [default])[0] 1882 1883 def getQualifiedParameter(request, calendar_name, argname, default=None): 1884 1885 """ 1886 Using the given 'request', 'calendar_name' and 'argname', retrieve the 1887 value of the qualified parameter, returning the optional 'default' (or None) 1888 if no value was supplied in the 'request'. 1889 """ 1890 1891 argname = getQualifiedParameterName(calendar_name, argname) 1892 return getParameter(request, argname, default) 1893 1894 def getQualifiedParameterName(calendar_name, argname): 1895 1896 """ 1897 Return the qualified parameter name using the given 'calendar_name' and 1898 'argname'. 1899 """ 1900 1901 if calendar_name is None: 1902 return argname 1903 else: 1904 return "%s-%s" % (calendar_name, argname) 1905 1906 def getParameterDate(arg): 1907 1908 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1909 1910 n = None 1911 1912 if arg is None: 1913 return None 1914 1915 elif arg.startswith("current"): 1916 date = getCurrentDate() 1917 if len(arg) > 8: 1918 n = int(arg[7:]) 1919 1920 elif arg.startswith("yearstart"): 1921 date = Date((getCurrentYear(), 1, 1)) 1922 if len(arg) > 10: 1923 n = int(arg[9:]) 1924 1925 elif arg.startswith("yearend"): 1926 date = Date((getCurrentYear(), 12, 31)) 1927 if len(arg) > 8: 1928 n = int(arg[7:]) 1929 1930 else: 1931 date = getDate(arg) 1932 1933 if n is not None: 1934 date = date.day_update(n) 1935 1936 return date 1937 1938 def getParameterMonth(arg): 1939 1940 "Interpret 'arg', recognising keywords and simple arithmetic operations." 1941 1942 n = None 1943 1944 if arg is None: 1945 return None 1946 1947 elif arg.startswith("current"): 1948 date = getCurrentMonth() 1949 if len(arg) > 8: 1950 n = int(arg[7:]) 1951 1952 elif arg.startswith("yearstart"): 1953 date = Month((getCurrentYear(), 1)) 1954 if len(arg) > 10: 1955 n = int(arg[9:]) 1956 1957 elif arg.startswith("yearend"): 1958 date = Month((getCurrentYear(), 12)) 1959 if len(arg) > 8: 1960 n = int(arg[7:]) 1961 1962 else: 1963 date = getMonth(arg) 1964 1965 if n is not None: 1966 date = date.month_update(n) 1967 1968 return date 1969 1970 def getFormDate(request, calendar_name, argname): 1971 1972 """ 1973 Return the date from the 'request' for the calendar with the given 1974 'calendar_name' using the parameter having the given 'argname'. 1975 """ 1976 1977 arg = getQualifiedParameter(request, calendar_name, argname) 1978 return getParameterDate(arg) 1979 1980 def getFormMonth(request, calendar_name, argname): 1981 1982 """ 1983 Return the month from the 'request' for the calendar with the given 1984 'calendar_name' using the parameter having the given 'argname'. 1985 """ 1986 1987 arg = getQualifiedParameter(request, calendar_name, argname) 1988 return getParameterMonth(arg) 1989 1990 def getFormDateTriple(request, yeararg, montharg, dayarg): 1991 1992 """ 1993 Return the date from the 'request' for the calendar with the given 1994 'calendar_name' using the parameters having the given 'yeararg', 'montharg' 1995 and 'dayarg' names. 1996 """ 1997 1998 year = getParameter(request, yeararg) 1999 month = getParameter(request, montharg) 2000 day = getParameter(request, dayarg) 2001 if year and month and day: 2002 return Date((int(year), int(month), int(day))) 2003 else: 2004 return None 2005 2006 def getFormMonthPair(request, yeararg, montharg): 2007 2008 """ 2009 Return the month from the 'request' for the calendar with the given 2010 'calendar_name' using the parameters having the given 'yeararg' and 2011 'montharg' names. 2012 """ 2013 2014 year = getParameter(request, yeararg) 2015 month = getParameter(request, montharg) 2016 if year and month: 2017 return Month((int(year), int(month))) 2018 else: 2019 return None 2020 2021 def getFullDateLabel(request, date): 2022 2023 """ 2024 Return the full month plus year label using the given 'request' and 2025 'year_month'. 2026 """ 2027 2028 if not date: 2029 return "" 2030 2031 _ = request.getText 2032 year, month, day = date.as_tuple()[:3] 2033 start_weekday, number_of_days = date.month_properties() 2034 weekday = (start_weekday + day - 1) % 7 2035 day_label = _(getDayLabel(weekday)) 2036 month_label = _(getMonthLabel(month)) 2037 return "%s %s %s %s" % (day_label, day, month_label, year) 2038 2039 def getFullMonthLabel(request, year_month): 2040 2041 """ 2042 Return the full month plus year label using the given 'request' and 2043 'year_month'. 2044 """ 2045 2046 if not year_month: 2047 return "" 2048 2049 _ = request.getText 2050 year, month = year_month.as_tuple()[:2] 2051 month_label = _(getMonthLabel(month)) 2052 return "%s %s" % (month_label, year) 2053 2054 # Page-related functions. 2055 2056 def getPrettyPageName(page): 2057 2058 "Return a nicely formatted title/name for the given 'page'." 2059 2060 title = page.split_title(force=1) 2061 return getPrettyTitle(title) 2062 2063 def linkToPage(request, page, text, query_string=None): 2064 2065 """ 2066 Using 'request', return a link to 'page' with the given link 'text' and 2067 optional 'query_string'. 2068 """ 2069 2070 text = wikiutil.escape(text) 2071 return page.link_to_raw(request, text, query_string) 2072 2073 def getFullPageName(parent, title): 2074 2075 """ 2076 Return a full page name from the given 'parent' page (can be empty or None) 2077 and 'title' (a simple page name). 2078 """ 2079 2080 if parent: 2081 return "%s/%s" % (parent.rstrip("/"), title) 2082 else: 2083 return title 2084 2085 def fillEventPageFromTemplate(template_page, new_page, event_details, category_pagenames): 2086 2087 """ 2088 Using the given 'template_page', complete the 'new_page' by copying the 2089 template and adding the given 'event_details' (a dictionary of event 2090 fields), setting also the 'category_pagenames' to define category 2091 membership. 2092 """ 2093 2094 event_page = EventPage(template_page) 2095 new_event_page = EventPage(new_page) 2096 new_event_page.copyPage(event_page) 2097 2098 if new_event_page.getFormat() == "wiki": 2099 new_event = Event(new_event_page, event_details) 2100 new_event_page.setEvents([new_event]) 2101 new_event_page.setCategoryMembership(category_pagenames) 2102 new_event_page.saveChanges() 2103 2104 # Formatting-related functions. 2105 2106 def getParserClass(request, format): 2107 2108 """ 2109 Return a parser class using the 'request' for the given 'format', returning 2110 a plain text parser if no parser can be found for the specified 'format'. 2111 """ 2112 2113 try: 2114 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 2115 except wikiutil.PluginMissingError: 2116 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 2117 2118 def getFormatter(request, mimetype, page): 2119 2120 """ 2121 Return a formatter using the given 'request' for the given 'mimetype' for 2122 use on the indicated 'page'. 2123 """ 2124 2125 try: 2126 cls = wikiutil.searchAndImportPlugin(request.cfg, "formatter", mimetype) 2127 except wikiutil.PluginMissingError: 2128 cls = wikiutil.searchAndImportPlugin(request.cfg, "formatter", "text/plain") 2129 fmt = request.formatter = page.formatter = cls(request) 2130 fmt.setPage(page) 2131 return fmt 2132 2133 def formatText(text, request, fmt, parser_cls): 2134 2135 """ 2136 Format the given 'text' using the specified 'request', formatter 'fmt' and 2137 parser class 'parser_cls'. 2138 """ 2139 2140 # Suppress line anchors. 2141 2142 parser = parser_cls(text, request, line_anchors=False) 2143 2144 # Fix lists by indicating that a paragraph is already started. 2145 2146 return request.redirectedOutput(parser.format, fmt, inhibit_p=True) 2147 2148 # vim: tabstop=4 expandtab shiftwidth=4