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