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