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