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