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