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