1 #!/usr/bin/env python 2 3 """ 4 A simple filesystem-based store of calendar data. 5 6 Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from imiptools.stores import StoreBase, PublisherBase, JournalBase 23 24 from datetime import datetime 25 from imiptools.config import STORE_DIR, PUBLISH_DIR, JOURNAL_DIR 26 from imiptools.data import make_calendar, parse_object, to_stream 27 from imiptools.dates import format_datetime, get_datetime, to_timezone 28 from imiptools.filesys import fix_permissions, FileBase 29 from imiptools.period import FreeBusyPeriod, FreeBusyCollection 30 from imiptools.text import parse_line 31 from os.path import isdir, isfile, join 32 from os import listdir, remove, rmdir 33 import codecs 34 35 class FileStoreBase(FileBase): 36 37 "A file store supporting user-specific locking and tabular data." 38 39 def acquire_lock(self, user, timeout=None): 40 FileBase.acquire_lock(self, timeout, user) 41 42 def release_lock(self, user): 43 FileBase.release_lock(self, user) 44 45 # Utility methods. 46 47 def _set_defaults(self, t, empty_defaults): 48 for i, default in empty_defaults: 49 if i >= len(t): 50 t += [None] * (i - len(t) + 1) 51 if not t[i]: 52 t[i] = default 53 return t 54 55 def _get_table(self, user, filename, empty_defaults=None, tab_separated=True): 56 57 """ 58 From the file for the given 'user' having the given 'filename', return 59 a list of tuples representing the file's contents. 60 61 The 'empty_defaults' is a list of (index, value) tuples indicating the 62 default value where a column either does not exist or provides an empty 63 value. 64 65 If 'tab_separated' is specified and is a false value, line parsing using 66 the imiptools.text.parse_line function will be performed instead of 67 splitting each line of the file using tab characters as separators. 68 """ 69 70 f = codecs.open(filename, "rb", encoding="utf-8") 71 try: 72 l = [] 73 for line in f.readlines(): 74 line = line.strip(" \r\n") 75 if tab_separated: 76 t = line.split("\t") 77 else: 78 t = parse_line(line) 79 if empty_defaults: 80 t = self._set_defaults(t, empty_defaults) 81 l.append(tuple(t)) 82 return l 83 finally: 84 f.close() 85 86 def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True): 87 88 """ 89 From the file for the given 'user' having the given 'filename', return 90 a list of tuples representing the file's contents. 91 92 The 'empty_defaults' is a list of (index, value) tuples indicating the 93 default value where a column either does not exist or provides an empty 94 value. 95 96 If 'tab_separated' is specified and is a false value, line parsing using 97 the imiptools.text.parse_line function will be performed instead of 98 splitting each line of the file using tab characters as separators. 99 """ 100 101 self.acquire_lock(user) 102 try: 103 return self._get_table(user, filename, empty_defaults, tab_separated) 104 finally: 105 self.release_lock(user) 106 107 def _set_table(self, user, filename, items, empty_defaults=None): 108 109 """ 110 For the given 'user', write to the file having the given 'filename' the 111 'items'. 112 113 The 'empty_defaults' is a list of (index, value) tuples indicating the 114 default value where a column either does not exist or provides an empty 115 value. 116 """ 117 118 f = codecs.open(filename, "wb", encoding="utf-8") 119 try: 120 for item in items: 121 self._set_table_item(f, item, empty_defaults) 122 finally: 123 f.close() 124 fix_permissions(filename) 125 126 def _set_table_item(self, f, item, empty_defaults=None): 127 128 "Set in table 'f' the given 'item', using any 'empty_defaults'." 129 130 if empty_defaults: 131 item = self._set_defaults(list(item), empty_defaults) 132 f.write("\t".join(item) + "\n") 133 134 def _set_table_atomic(self, user, filename, items, empty_defaults=None): 135 136 """ 137 For the given 'user', write to the file having the given 'filename' the 138 'items'. 139 140 The 'empty_defaults' is a list of (index, value) tuples indicating the 141 default value where a column either does not exist or provides an empty 142 value. 143 """ 144 145 self.acquire_lock(user) 146 try: 147 self._set_table(user, filename, items, empty_defaults) 148 finally: 149 self.release_lock(user) 150 151 class FileStore(FileStoreBase, StoreBase): 152 153 "A file store of tabular free/busy data and objects." 154 155 def __init__(self, store_dir=None): 156 FileBase.__init__(self, store_dir or STORE_DIR) 157 158 # Store object access. 159 160 def _get_object(self, user, filename): 161 162 """ 163 Return the parsed object for the given 'user' having the given 164 'filename'. 165 """ 166 167 self.acquire_lock(user) 168 try: 169 f = open(filename, "rb") 170 try: 171 return parse_object(f, "utf-8") 172 finally: 173 f.close() 174 finally: 175 self.release_lock(user) 176 177 def _set_object(self, user, filename, node): 178 179 """ 180 Set an object for the given 'user' having the given 'filename', using 181 'node' to define the object. 182 """ 183 184 self.acquire_lock(user) 185 try: 186 f = open(filename, "wb") 187 try: 188 to_stream(f, node) 189 finally: 190 f.close() 191 fix_permissions(filename) 192 finally: 193 self.release_lock(user) 194 195 return True 196 197 def _remove_object(self, filename): 198 199 "Remove the object with the given 'filename'." 200 201 try: 202 remove(filename) 203 except OSError: 204 return False 205 206 return True 207 208 def _remove_collection(self, filename): 209 210 "Remove the collection with the given 'filename'." 211 212 try: 213 rmdir(filename) 214 except OSError: 215 return False 216 217 return True 218 219 # User discovery. 220 221 def get_users(self): 222 223 "Return a list of users." 224 225 return listdir(self.store_dir) 226 227 # Event and event metadata access. 228 229 def get_events(self, user): 230 231 "Return a list of event identifiers." 232 233 filename = self.get_object_in_store(user, "objects") 234 if not filename or not isdir(filename): 235 return None 236 237 return [name for name in listdir(filename) if isfile(join(filename, name))] 238 239 def get_event_filename(self, user, uid, recurrenceid=None, dirname=None, username=None): 240 241 """ 242 Get the filename providing the event for the given 'user' with the given 243 'uid'. If the optional 'recurrenceid' is specified, a specific instance 244 or occurrence of an event is returned. 245 246 Where 'dirname' is specified, the given directory name is used as the 247 base of the location within which any filename will reside. 248 """ 249 250 if recurrenceid: 251 return self.get_recurrence_filename(user, uid, recurrenceid, dirname, username) 252 else: 253 return self.get_complete_event_filename(user, uid, dirname, username) 254 255 def get_event(self, user, uid, recurrenceid=None, dirname=None): 256 257 """ 258 Get the event for the given 'user' with the given 'uid'. If 259 the optional 'recurrenceid' is specified, a specific instance or 260 occurrence of an event is returned. 261 """ 262 263 filename = self.get_event_filename(user, uid, recurrenceid, dirname) 264 if not filename or not isfile(filename): 265 return None 266 267 return filename and self._get_object(user, filename) 268 269 def get_complete_event_filename(self, user, uid, dirname=None, username=None): 270 271 """ 272 Get the filename providing the event for the given 'user' with the given 273 'uid'. 274 275 Where 'dirname' is specified, the given directory name is used as the 276 base of the location within which any filename will reside. 277 278 Where 'username' is specified, the event details will reside in a file 279 bearing that name within a directory having 'uid' as its name. 280 """ 281 282 return self.get_object_in_store(user, dirname, "objects", uid, username) 283 284 def get_complete_event(self, user, uid): 285 286 "Get the event for the given 'user' with the given 'uid'." 287 288 filename = self.get_complete_event_filename(user, uid) 289 if not filename or not isfile(filename): 290 return None 291 292 return filename and self._get_object(user, filename) 293 294 def set_complete_event(self, user, uid, node): 295 296 "Set an event for 'user' having the given 'uid' and 'node'." 297 298 filename = self.get_object_in_store(user, "objects", uid) 299 if not filename: 300 return False 301 302 return self._set_object(user, filename, node) 303 304 def remove_parent_event(self, user, uid): 305 306 "Remove the parent event for 'user' having the given 'uid'." 307 308 filename = self.get_object_in_store(user, "objects", uid) 309 if not filename: 310 return False 311 312 return self._remove_object(filename) 313 314 def get_recurrences(self, user, uid): 315 316 """ 317 Get additional event instances for an event of the given 'user' with the 318 indicated 'uid'. Both active and cancelled recurrences are returned. 319 """ 320 321 return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid) 322 323 def get_active_recurrences(self, user, uid): 324 325 """ 326 Get additional event instances for an event of the given 'user' with the 327 indicated 'uid'. Cancelled recurrences are not returned. 328 """ 329 330 filename = self.get_object_in_store(user, "recurrences", uid) 331 if not filename or not isdir(filename): 332 return [] 333 334 return [name for name in listdir(filename) if isfile(join(filename, name))] 335 336 def get_cancelled_recurrences(self, user, uid): 337 338 """ 339 Get additional event instances for an event of the given 'user' with the 340 indicated 'uid'. Only cancelled recurrences are returned. 341 """ 342 343 filename = self.get_object_in_store(user, "cancellations", "recurrences", uid) 344 if not filename or not isdir(filename): 345 return [] 346 347 return [name for name in listdir(filename) if isfile(join(filename, name))] 348 349 def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None, username=None): 350 351 """ 352 For the event of the given 'user' with the given 'uid', return the 353 filename providing the recurrence with the given 'recurrenceid'. 354 355 Where 'dirname' is specified, the given directory name is used as the 356 base of the location within which any filename will reside. 357 358 Where 'username' is specified, the event details will reside in a file 359 bearing that name within a directory having 'uid' as its name. 360 """ 361 362 return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid, username) 363 364 def get_recurrence(self, user, uid, recurrenceid): 365 366 """ 367 For the event of the given 'user' with the given 'uid', return the 368 specific recurrence indicated by the 'recurrenceid'. 369 """ 370 371 filename = self.get_recurrence_filename(user, uid, recurrenceid) 372 if not filename or not isfile(filename): 373 return None 374 375 return filename and self._get_object(user, filename) 376 377 def set_recurrence(self, user, uid, recurrenceid, node): 378 379 "Set an event for 'user' having the given 'uid' and 'node'." 380 381 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 382 if not filename: 383 return False 384 385 return self._set_object(user, filename, node) 386 387 def remove_recurrence(self, user, uid, recurrenceid): 388 389 """ 390 Remove a special recurrence from an event stored by 'user' having the 391 given 'uid' and 'recurrenceid'. 392 """ 393 394 filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid) 395 if not filename: 396 return False 397 398 return self._remove_object(filename) 399 400 def remove_recurrence_collection(self, user, uid): 401 402 """ 403 Remove the collection of recurrences stored by 'user' having the given 404 'uid'. 405 """ 406 407 recurrences = self.get_object_in_store(user, "recurrences", uid) 408 if recurrences: 409 return self._remove_collection(recurrences) 410 411 return True 412 413 # Free/busy period providers, upon extension of the free/busy records. 414 415 def _get_freebusy_providers(self, user): 416 417 """ 418 Return the free/busy providers for the given 'user'. 419 420 This function returns any stored datetime and a list of providers as a 421 2-tuple. Each provider is itself a (uid, recurrenceid) tuple. 422 """ 423 424 filename = self.get_object_in_store(user, "freebusy-providers") 425 if not filename or not isfile(filename): 426 return None 427 428 # Attempt to read providers, with a declaration of the datetime 429 # from which such providers are considered as still being active. 430 431 t = self._get_table_atomic(user, filename, [(1, None)]) 432 try: 433 dt_string = t[0][0] 434 except IndexError: 435 return None 436 437 return dt_string, t[1:] 438 439 def _set_freebusy_providers(self, user, dt_string, t): 440 441 "Set the given provider timestamp 'dt_string' and table 't'." 442 443 filename = self.get_object_in_store(user, "freebusy-providers") 444 if not filename: 445 return False 446 447 t.insert(0, (dt_string,)) 448 self._set_table_atomic(user, filename, t, [(1, "")]) 449 return True 450 451 # Free/busy period access. 452 453 def get_freebusy(self, user, name=None, mutable=False): 454 455 "Get free/busy details for the given 'user'." 456 457 filename = self.get_object_in_store(user, name or "freebusy") 458 459 if not filename or not isfile(filename): 460 periods = [] 461 else: 462 periods = map(lambda t: FreeBusyPeriod(*t), 463 self._get_table_atomic(user, filename)) 464 465 return FreeBusyCollection(periods, mutable) 466 467 def get_freebusy_for_update(self, user, name=None): 468 469 "Get free/busy details for the given 'user'." 470 471 return self.get_freebusy(user, name, True) 472 473 def get_freebusy_for_other(self, user, other, mutable=False): 474 475 "For the given 'user', get free/busy details for the 'other' user." 476 477 filename = self.get_object_in_store(user, "freebusy-other", other) 478 479 if not filename or not isfile(filename): 480 periods = [] 481 else: 482 periods = map(lambda t: FreeBusyPeriod(*t), 483 self._get_table_atomic(user, filename)) 484 485 return FreeBusyCollection(periods, mutable) 486 487 def get_freebusy_for_other_for_update(self, user, other): 488 489 "For the given 'user', get free/busy details for the 'other' user." 490 491 return self.get_freebusy_for_other(user, other, True) 492 493 def set_freebusy(self, user, freebusy, name=None): 494 495 "For the given 'user', set 'freebusy' details." 496 497 filename = self.get_object_in_store(user, name or "freebusy") 498 if not filename: 499 return False 500 501 self._set_table_atomic(user, filename, 502 map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods)) 503 return True 504 505 def set_freebusy_for_other(self, user, freebusy, other): 506 507 "For the given 'user', set 'freebusy' details for the 'other' user." 508 509 filename = self.get_object_in_store(user, "freebusy-other", other) 510 if not filename: 511 return False 512 513 self._set_table_atomic(user, filename, 514 map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods)) 515 return True 516 517 # Tentative free/busy periods related to countering. 518 519 def get_freebusy_offers(self, user, mutable=False): 520 521 "Get free/busy offers for the given 'user'." 522 523 offers = [] 524 expired = [] 525 now = to_timezone(datetime.utcnow(), "UTC") 526 527 # Expire old offers and save the collection if modified. 528 529 self.acquire_lock(user) 530 try: 531 l = self.get_freebusy(user, "freebusy-offers") 532 for fb in l: 533 if fb.expires and get_datetime(fb.expires) <= now: 534 expired.append(fb) 535 else: 536 offers.append(fb) 537 538 if expired: 539 self.set_freebusy_offers(user, offers) 540 finally: 541 self.release_lock(user) 542 543 return FreeBusyCollection(offers, mutable) 544 545 def get_freebusy_offers_for_update(self, user): 546 547 "Get free/busy offers for the given 'user'." 548 549 return self.get_freebusy_offers(user, True) 550 551 def set_freebusy_offers(self, user, freebusy): 552 553 "For the given 'user', set 'freebusy' offers." 554 555 return self.set_freebusy(user, freebusy, "freebusy-offers") 556 557 # Requests and counter-proposals. 558 559 def _get_requests(self, user, queue): 560 561 "Get requests for the given 'user' from the given 'queue'." 562 563 filename = self.get_object_in_store(user, queue) 564 if not filename or not isfile(filename): 565 return None 566 567 return self._get_table_atomic(user, filename, [(1, None), (2, None)]) 568 569 def get_requests(self, user): 570 571 "Get requests for the given 'user'." 572 573 return self._get_requests(user, "requests") 574 575 def _set_requests(self, user, requests, queue): 576 577 """ 578 For the given 'user', set the list of queued 'requests' in the given 579 'queue'. 580 """ 581 582 filename = self.get_object_in_store(user, queue) 583 if not filename: 584 return False 585 586 self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")]) 587 return True 588 589 def set_requests(self, user, requests): 590 591 "For the given 'user', set the list of queued 'requests'." 592 593 return self._set_requests(user, requests, "requests") 594 595 def _set_request(self, user, request, queue): 596 597 """ 598 For the given 'user', set the given 'request' in the given 'queue'. 599 """ 600 601 filename = self.get_object_in_store(user, queue) 602 if not filename: 603 return False 604 605 self.acquire_lock(user) 606 try: 607 f = codecs.open(filename, "ab", encoding="utf-8") 608 try: 609 self._set_table_item(f, request, [(1, ""), (2, "")]) 610 finally: 611 f.close() 612 fix_permissions(filename) 613 finally: 614 self.release_lock(user) 615 616 return True 617 618 def set_request(self, user, uid, recurrenceid=None, type=None): 619 620 """ 621 For the given 'user', set the queued 'uid' and 'recurrenceid', 622 indicating a request, along with any given 'type'. 623 """ 624 625 return self._set_request(user, (uid, recurrenceid, type), "requests") 626 627 def get_counters(self, user, uid, recurrenceid=None): 628 629 """ 630 For the given 'user', return a list of users from whom counter-proposals 631 have been received for the given 'uid' and optional 'recurrenceid'. 632 """ 633 634 filename = self.get_event_filename(user, uid, recurrenceid, "counters") 635 if not filename or not isdir(filename): 636 return False 637 638 return [name for name in listdir(filename) if isfile(join(filename, name))] 639 640 def get_counter(self, user, other, uid, recurrenceid=None): 641 642 """ 643 For the given 'user', return the counter-proposal from 'other' for the 644 given 'uid' and optional 'recurrenceid'. 645 """ 646 647 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 648 if not filename: 649 return False 650 651 return self._get_object(user, filename) 652 653 def set_counter(self, user, other, node, uid, recurrenceid=None): 654 655 """ 656 For the given 'user', store a counter-proposal received from 'other' the 657 given 'node' representing that proposal for the given 'uid' and 658 'recurrenceid'. 659 """ 660 661 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 662 if not filename: 663 return False 664 665 return self._set_object(user, filename, node) 666 667 def remove_counters(self, user, uid, recurrenceid=None): 668 669 """ 670 For the given 'user', remove all counter-proposals associated with the 671 given 'uid' and 'recurrenceid'. 672 """ 673 674 filename = self.get_event_filename(user, uid, recurrenceid, "counters") 675 if not filename or not isdir(filename): 676 return False 677 678 removed = False 679 680 for other in listdir(filename): 681 counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 682 removed = removed or self._remove_object(counter_filename) 683 684 return removed 685 686 def remove_counter(self, user, other, uid, recurrenceid=None): 687 688 """ 689 For the given 'user', remove any counter-proposal from 'other' 690 associated with the given 'uid' and 'recurrenceid'. 691 """ 692 693 filename = self.get_event_filename(user, uid, recurrenceid, "counters", other) 694 if not filename or not isfile(filename): 695 return False 696 697 return self._remove_object(filename) 698 699 # Event cancellation. 700 701 def cancel_event(self, user, uid, recurrenceid=None): 702 703 """ 704 Cancel an event for 'user' having the given 'uid'. If the optional 705 'recurrenceid' is specified, a specific instance or occurrence of an 706 event is cancelled. 707 """ 708 709 filename = self.get_event_filename(user, uid, recurrenceid) 710 cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 711 712 if filename and cancelled_filename and isfile(filename): 713 return self.move_object(filename, cancelled_filename) 714 715 return False 716 717 def uncancel_event(self, user, uid, recurrenceid=None): 718 719 """ 720 Uncancel an event for 'user' having the given 'uid'. If the optional 721 'recurrenceid' is specified, a specific instance or occurrence of an 722 event is uncancelled. 723 """ 724 725 filename = self.get_event_filename(user, uid, recurrenceid) 726 cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 727 728 if filename and cancelled_filename and isfile(cancelled_filename): 729 return self.move_object(cancelled_filename, filename) 730 731 return False 732 733 def remove_cancellation(self, user, uid, recurrenceid=None): 734 735 """ 736 Remove a cancellation for 'user' for the event having the given 'uid'. 737 If the optional 'recurrenceid' is specified, a specific instance or 738 occurrence of an event is affected. 739 """ 740 741 # Remove any parent event cancellation or a specific recurrence 742 # cancellation if indicated. 743 744 filename = self.get_event_filename(user, uid, recurrenceid, "cancellations") 745 746 if filename and isfile(filename): 747 return self._remove_object(filename) 748 749 return False 750 751 class FilePublisher(FileBase, PublisherBase): 752 753 "A publisher of objects." 754 755 def __init__(self, store_dir=None): 756 FileBase.__init__(self, store_dir or PUBLISH_DIR) 757 758 def set_freebusy(self, user, freebusy): 759 760 "For the given 'user', set 'freebusy' details." 761 762 filename = self.get_object_in_store(user, "freebusy") 763 if not filename: 764 return False 765 766 record = [] 767 rwrite = record.append 768 769 rwrite(("ORGANIZER", {}, user)) 770 rwrite(("UID", {}, user)) 771 rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ"))) 772 773 for fb in freebusy: 774 if not fb.transp or fb.transp == "OPAQUE": 775 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 776 map(format_datetime, [fb.get_start_point(), fb.get_end_point()])))) 777 778 f = open(filename, "wb") 779 try: 780 to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH")) 781 finally: 782 f.close() 783 fix_permissions(filename) 784 785 return True 786 787 class FileJournal(FileStoreBase, JournalBase): 788 789 "A journal system to support quotas." 790 791 def __init__(self, store_dir=None): 792 FileBase.__init__(self, store_dir or JOURNAL_DIR) 793 794 # Quota and user identity/group discovery. 795 796 def get_quotas(self): 797 798 "Return a list of quotas." 799 800 return listdir(self.store_dir) 801 802 def get_quota_users(self, quota): 803 804 "Return a list of quota users." 805 806 filename = self.get_object_in_store(quota, "journal") 807 if not filename or not isdir(filename): 808 return [] 809 810 return listdir(filename) 811 812 # Groups of users sharing quotas. 813 814 def get_groups(self, quota): 815 816 "Return the identity mappings for the given 'quota' as a dictionary." 817 818 filename = self.get_object_in_store(quota, "groups") 819 if not filename or not isfile(filename): 820 return {} 821 822 return dict(self._get_table_atomic(quota, filename, tab_separated=False)) 823 824 def get_limits(self, quota): 825 826 """ 827 Return the limits for the 'quota' as a dictionary mapping identities or 828 groups to durations. 829 """ 830 831 filename = self.get_object_in_store(quota, "limits") 832 if not filename or not isfile(filename): 833 return None 834 835 return dict(self._get_table_atomic(quota, filename, tab_separated=False)) 836 837 # Free/busy period access for users within quota groups. 838 839 def get_freebusy(self, quota, user, mutable=False): 840 841 "Get free/busy details for the given 'quota' and 'user'." 842 843 filename = self.get_object_in_store(quota, "freebusy", user) 844 845 if not filename or not isfile(filename): 846 periods = [] 847 else: 848 periods = map(lambda t: FreeBusyPeriod(*t), 849 self._get_table_atomic(quota, filename)) 850 851 return FreeBusyCollection(periods, mutable) 852 853 def get_freebusy_for_update(self, quota, user): 854 855 "Get free/busy details for the given 'quota' and 'user'." 856 857 return self.get_freebusy(quota, user, True) 858 859 def set_freebusy(self, quota, user, freebusy): 860 861 "For the given 'quota' and 'user', set 'freebusy' details." 862 863 filename = self.get_object_in_store(quota, "freebusy", user) 864 if not filename: 865 return False 866 867 self._set_table_atomic(quota, filename, 868 map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods)) 869 return True 870 871 # Journal entry methods. 872 873 def get_entries(self, quota, group, mutable=False): 874 875 """ 876 Return a list of journal entries for the given 'quota' for the indicated 877 'group'. 878 """ 879 880 filename = self.get_object_in_store(quota, "journal", group) 881 882 if not filename or not isfile(filename): 883 periods = [] 884 else: 885 periods = map(lambda t: FreeBusyPeriod(*t), 886 self._get_table_atomic(quota, filename)) 887 888 return FreeBusyCollection(periods, mutable) 889 890 def get_entries_for_update(self, quota, group): 891 892 """ 893 Return a list of journal entries for the given 'quota' for the indicated 894 'group'. 895 """ 896 897 return self.get_entries(quota, group, True) 898 899 def set_entries(self, quota, group, entries): 900 901 """ 902 For the given 'quota' and indicated 'group', set the list of journal 903 'entries'. 904 """ 905 906 filename = self.get_object_in_store(quota, "journal", group) 907 if not filename: 908 return False 909 910 self._set_table_atomic(quota, filename, 911 map(lambda fb: fb.as_tuple(strings_only=True), entries.periods)) 912 return True 913 914 # vim: tabstop=4 expandtab shiftwidth=4