imip-agent

imiptools/stores/database/common.py

1119:97b5330fa753
2016-04-18 Paul Boddie Merged changes from the default branch. freebusy-collections
     1 #!/usr/bin/env python     2      3 """     4 A database 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, JournalBase    23     24 from datetime import datetime    25 from imiptools.data import parse_string, to_string    26 from imiptools.dates import format_datetime, get_datetime, to_timezone    27 from imiptools.period import FreeBusyDatabaseCollection    28 from imiptools.sql import DatabaseOperations    29     30 class DatabaseStoreBase(DatabaseOperations):    31     32     "A database store supporting user-specific locking."    33     34     def __init__(self, connection, paramstyle=None):    35         DatabaseOperations.__init__(self, paramstyle=paramstyle)    36         self.connection = connection    37         self.cursor = connection.cursor()    38     39     def acquire_lock(self, user, timeout=None):    40         pass    41     42     def release_lock(self, user):    43         pass    44     45 class DatabaseStore(DatabaseStoreBase, StoreBase):    46     47     "A database store of tabular free/busy data and objects."    48     49     # User discovery.    50     51     def get_users(self):    52     53         "Return a list of users."    54     55         query = "select distinct store_user from freebusy"    56         self.cursor.execute(query)    57         return [r[0] for r in self.cursor.fetchall()]    58     59     # Event and event metadata access.    60     61     def get_events(self, user):    62     63         "Return a list of event identifiers."    64     65         columns = ["store_user", "status"]    66         values = [user, "active"]    67     68         query, values = self.get_query(    69             "select object_uid from objects :condition",    70             columns, values)    71     72         self.cursor.execute(query, values)    73         return [r[0] for r in self.cursor.fetchall()]    74     75     def get_all_events(self, user):    76     77         "Return a set of (uid, recurrenceid) tuples for all events."    78     79         query, values = self.get_query(    80             "select object_uid, null as object_recurrenceid from objects :condition "    81             "union all "    82             "select object_uid, object_recurrenceid from recurrences :condition",    83             ["store_user"], [user])    84     85         self.cursor.execute(query, values)    86         return self.cursor.fetchall()    87     88     def get_event_table(self, recurrenceid=None, dirname=None):    89     90         "Get the table providing events for any specified 'dirname'."    91     92         if recurrenceid:    93             return self.get_recurrence_table(dirname)    94         else:    95             return self.get_complete_event_table(dirname)    96     97     def get_event_table_filters(self, dirname=None):    98     99         "Get filter details for any specified 'dirname'."   100    101         if dirname == "cancellations":   102             return ["status"], ["cancelled"]   103         else:   104             return ["status"], ["active"]   105    106     def get_event(self, user, uid, recurrenceid=None, dirname=None):   107    108         """   109         Get the event for the given 'user' with the given 'uid'. If   110         the optional 'recurrenceid' is specified, a specific instance or   111         occurrence of an event is returned.   112         """   113    114         table = self.get_event_table(recurrenceid, dirname)   115         columns, values = self.get_event_table_filters(dirname)   116    117         if recurrenceid:   118             columns += ["store_user", "object_uid", "object_recurrenceid"]   119             values += [user, uid, recurrenceid]   120         else:   121             columns += ["store_user", "object_uid"]   122             values += [user, uid]   123    124         query, values = self.get_query(   125             "select object_text from %(table)s :condition" % {   126                 "table" : table   127                 },   128             columns, values)   129    130         self.cursor.execute(query, values)   131         result = self.cursor.fetchone()   132         return result and parse_string(result[0], "utf-8")   133    134     def get_complete_event_table(self, dirname=None):   135    136         "Get the table providing events for any specified 'dirname'."   137    138         if dirname == "counters":   139             return "countered_objects"   140         else:   141             return "objects"   142    143     def get_complete_event(self, user, uid):   144    145         "Get the event for the given 'user' with the given 'uid'."   146    147         columns = ["store_user", "object_uid"]   148         values = [user, uid]   149    150         query, values = self.get_query(   151             "select object_text from objects :condition",   152             columns, values)   153    154         self.cursor.execute(query, values)   155         result = self.cursor.fetchone()   156         return result and parse_string(result[0], "utf-8")   157    158     def set_complete_event(self, user, uid, node):   159    160         "Set an event for 'user' having the given 'uid' and 'node'."   161    162         columns = ["store_user", "object_uid"]   163         values = [user, uid]   164         setcolumns = ["object_text", "status"]   165         setvalues = [to_string(node, "utf-8"), "active"]   166    167         query, values = self.get_query(   168             "update objects :set :condition",   169             columns, values, setcolumns, setvalues)   170    171         self.cursor.execute(query, values)   172    173         if self.cursor.rowcount > 0 or self.get_complete_event(user, uid):   174             return True   175    176         columns = ["store_user", "object_uid", "object_text", "status"]   177         values = [user, uid, to_string(node, "utf-8"), "active"]   178    179         query, values = self.get_query(   180             "insert into objects (:columns) values (:values)",   181             columns, values)   182    183         self.cursor.execute(query, values)   184         return True   185    186     def remove_parent_event(self, user, uid):   187    188         "Remove the parent event for 'user' having the given 'uid'."   189    190         columns = ["store_user", "object_uid"]   191         values = [user, uid]   192    193         query, values = self.get_query(   194             "delete from objects :condition",   195             columns, values)   196    197         self.cursor.execute(query, values)   198         return self.cursor.rowcount > 0   199    200     def get_active_recurrences(self, user, uid):   201    202         """   203         Get additional event instances for an event of the given 'user' with the   204         indicated 'uid'. Cancelled recurrences are not returned.   205         """   206    207         columns = ["store_user", "object_uid", "status"]   208         values = [user, uid, "active"]   209    210         query, values = self.get_query(   211             "select object_recurrenceid from recurrences :condition",   212             columns, values)   213    214         self.cursor.execute(query, values)   215         return [t[0] for t in self.cursor.fetchall() or []]   216    217     def get_cancelled_recurrences(self, user, uid):   218    219         """   220         Get additional event instances for an event of the given 'user' with the   221         indicated 'uid'. Only cancelled recurrences are returned.   222         """   223    224         columns = ["store_user", "object_uid", "status"]   225         values = [user, uid, "cancelled"]   226    227         query, values = self.get_query(   228             "select object_recurrenceid from recurrences :condition",   229             columns, values)   230    231         self.cursor.execute(query, values)   232         return [t[0] for t in self.cursor.fetchall() or []]   233    234     def get_recurrence_table(self, dirname=None):   235    236         "Get the table providing recurrences for any specified 'dirname'."   237    238         if dirname == "counters":   239             return "countered_recurrences"   240         else:   241             return "recurrences"   242    243     def get_recurrence(self, user, uid, recurrenceid):   244    245         """   246         For the event of the given 'user' with the given 'uid', return the   247         specific recurrence indicated by the 'recurrenceid'.   248         """   249    250         columns = ["store_user", "object_uid", "object_recurrenceid"]   251         values = [user, uid, recurrenceid]   252    253         query, values = self.get_query(   254             "select object_text from recurrences :condition",   255             columns, values)   256    257         self.cursor.execute(query, values)   258         result = self.cursor.fetchone()   259         return result and parse_string(result[0], "utf-8")   260    261     def set_recurrence(self, user, uid, recurrenceid, node):   262    263         "Set an event for 'user' having the given 'uid' and 'node'."   264    265         columns = ["store_user", "object_uid", "object_recurrenceid"]   266         values = [user, uid, recurrenceid]   267         setcolumns = ["object_text", "status"]   268         setvalues = [to_string(node, "utf-8"), "active"]   269    270         query, values = self.get_query(   271             "update recurrences :set :condition",   272             columns, values, setcolumns, setvalues)   273    274         self.cursor.execute(query, values)   275    276         if self.cursor.rowcount > 0 or self.get_recurrence(user, uid, recurrenceid):   277             return True   278    279         columns = ["store_user", "object_uid", "object_recurrenceid", "object_text", "status"]   280         values = [user, uid, recurrenceid, to_string(node, "utf-8"), "active"]   281    282         query, values = self.get_query(   283             "insert into recurrences (:columns) values (:values)",   284             columns, values)   285    286         self.cursor.execute(query, values)   287         return True   288    289     def remove_recurrence(self, user, uid, recurrenceid):   290    291         """   292         Remove a special recurrence from an event stored by 'user' having the   293         given 'uid' and 'recurrenceid'.   294         """   295    296         columns = ["store_user", "object_uid", "object_recurrenceid"]   297         values = [user, uid, recurrenceid]   298    299         query, values = self.get_query(   300             "delete from recurrences :condition",   301             columns, values)   302    303         self.cursor.execute(query, values)   304         return True   305    306     def remove_recurrences(self, user, uid):   307    308         """   309         Remove all recurrences for an event stored by 'user' having the given   310         'uid'.   311         """   312    313         columns = ["store_user", "object_uid"]   314         values = [user, uid]   315    316         query, values = self.get_query(   317             "delete from recurrences :condition",   318             columns, values)   319    320         self.cursor.execute(query, values)   321         return True   322    323     # Free/busy period providers, upon extension of the free/busy records.   324    325     def _get_freebusy_providers(self, user):   326    327         """   328         Return the free/busy providers for the given 'user'.   329    330         This function returns any stored datetime and a list of providers as a   331         2-tuple. Each provider is itself a (uid, recurrenceid) tuple.   332         """   333    334         columns = ["store_user"]   335         values = [user]   336    337         query, values = self.get_query(   338             "select object_uid, object_recurrenceid from freebusy_providers :condition",   339             columns, values)   340    341         self.cursor.execute(query, values)   342         providers = self.cursor.fetchall()   343    344         columns = ["store_user"]   345         values = [user]   346    347         query, values = self.get_query(   348             "select start from freebusy_provider_datetimes :condition",   349             columns, values)   350    351         self.cursor.execute(query, values)   352         result = self.cursor.fetchone()   353         dt_string = result and result[0]   354    355         return dt_string, providers   356    357     def _set_freebusy_providers(self, user, dt_string, t):   358    359         "Set the given provider timestamp 'dt_string' and table 't'."   360    361         # NOTE: Locking?   362    363         columns = ["store_user"]   364         values = [user]   365    366         query, values = self.get_query(   367             "delete from freebusy_providers :condition",   368             columns, values)   369    370         self.cursor.execute(query, values)   371    372         columns = ["store_user", "object_uid", "object_recurrenceid"]   373    374         for uid, recurrenceid in t:   375             values = [user, uid, recurrenceid]   376    377             query, values = self.get_query(   378                 "insert into freebusy_providers (:columns) values (:values)",   379                 columns, values)   380    381             self.cursor.execute(query, values)   382    383         columns = ["store_user"]   384         values = [user]   385         setcolumns = ["start"]   386         setvalues = [dt_string]   387    388         query, values = self.get_query(   389             "update freebusy_provider_datetimes :set :condition",   390             columns, values, setcolumns, setvalues)   391    392         self.cursor.execute(query, values)   393    394         if self.cursor.rowcount > 0:   395             return True   396    397         columns = ["store_user", "start"]   398         values = [user, dt_string]   399    400         query, values = self.get_query(   401             "insert into freebusy_provider_datetimes (:columns) values (:values)",   402             columns, values)   403    404         self.cursor.execute(query, values)   405         return True   406    407     # Free/busy period access.   408    409     def get_freebusy(self, user, name=None, mutable=False):   410    411         "Get free/busy details for the given 'user'."   412    413         table = name or "freebusy"   414         return FreeBusyDatabaseCollection(self.cursor, table, ["store_user"], [user], mutable, self.paramstyle)   415    416     def get_freebusy_for_other(self, user, other, mutable=False):   417    418         "For the given 'user', get free/busy details for the 'other' user."   419    420         table = "freebusy_other"   421         return FreeBusyDatabaseCollection(self.cursor, table, ["store_user", "other"], [user, other], mutable, self.paramstyle)   422    423     def set_freebusy(self, user, freebusy, name=None):   424    425         "For the given 'user', set 'freebusy' details."   426    427         table = name or "freebusy"   428    429         if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table:   430             fbc = FreeBusyDatabaseCollection(self.cursor, table, ["store_user"], [user], True, self.paramstyle)   431             fbc += freebusy   432    433         return True   434    435     def set_freebusy_for_other(self, user, freebusy, other):   436    437         "For the given 'user', set 'freebusy' details for the 'other' user."   438    439         table = "freebusy_other"   440    441         if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table:   442             fbc = FreeBusyDatabaseCollection(self.cursor, table, ["store_user", "other"], [user, other], True, self.paramstyle)   443             fbc += freebusy   444    445         return True   446    447     # Tentative free/busy periods related to countering.   448    449     def get_freebusy_offers(self, user, mutable=False):   450    451         "Get free/busy offers for the given 'user'."   452    453         # Expire old offers and save the collection if modified.   454    455         now = format_datetime(to_timezone(datetime.utcnow(), "UTC"))   456         columns = ["store_user", "expires"]   457         values = [user, now]   458    459         query, values = self.get_query(   460             "delete from freebusy_offers :condition",   461             columns, values)   462    463         self.cursor.execute(query, values)   464    465         return self.get_freebusy(user, "freebusy_offers", mutable)   466    467     def set_freebusy_offers(self, user, freebusy):   468    469         "For the given 'user', set 'freebusy' offers."   470    471         return self.set_freebusy(user, freebusy, "freebusy_offers")   472    473     # Requests and counter-proposals.   474    475     def get_requests(self, user):   476    477         "Get requests for the given 'user'."   478    479         columns = ["store_user"]   480         values = [user]   481    482         query, values = self.get_query(   483             "select object_uid, object_recurrenceid, request_type from requests :condition",   484             columns, values)   485    486         self.cursor.execute(query, values)   487         return self.cursor.fetchall()   488    489     def set_request(self, user, uid, recurrenceid=None, type=None):   490    491         """   492         For the given 'user', set the queued 'uid' and 'recurrenceid',   493         indicating a request, along with any given 'type'.   494         """   495    496         columns = ["store_user", "object_uid", "object_recurrenceid", "request_type"]   497         values = [user, uid, recurrenceid, type]   498    499         query, values = self.get_query(   500             "insert into requests (:columns) values (:values)",   501             columns, values)   502    503         self.cursor.execute(query, values)   504         return True   505    506     def queue_request(self, user, uid, recurrenceid=None, type=None):   507    508         """   509         Queue a request for 'user' having the given 'uid'. If the optional   510         'recurrenceid' is specified, the entry refers to a specific instance   511         or occurrence of an event. The 'type' parameter can be used to indicate   512         a specific type of request.   513         """   514    515         if recurrenceid:   516             columns = ["store_user", "object_uid", "object_recurrenceid"]   517             values = [user, uid, recurrenceid]   518         else:   519             columns = ["store_user", "object_uid"]   520             values = [user, uid]   521    522         setcolumns = ["request_type"]   523         setvalues = [type]   524    525         query, values = self.get_query(   526             "update requests :set :condition",   527             columns, values, setcolumns, setvalues)   528    529         self.cursor.execute(query, values)   530    531         if self.cursor.rowcount > 0:   532             return   533    534         self.set_request(user, uid, recurrenceid, type)   535    536     def dequeue_request(self, user, uid, recurrenceid=None):   537    538         """   539         Dequeue all requests for 'user' having the given 'uid'. If the optional   540         'recurrenceid' is specified, all requests for that specific instance or   541         occurrence of an event are dequeued.   542         """   543    544         if recurrenceid:   545             columns = ["store_user", "object_uid", "object_recurrenceid"]   546             values = [user, uid, recurrenceid]   547         else:   548             columns = ["store_user", "object_uid"]   549             values = [user, uid]   550    551         query, values = self.get_query(   552             "delete from requests :condition",   553             columns, values)   554    555         self.cursor.execute(query, values)   556         return True   557    558     def get_counters(self, user, uid, recurrenceid=None):   559    560         """   561         For the given 'user', return a list of users from whom counter-proposals   562         have been received for the given 'uid' and optional 'recurrenceid'.   563         """   564    565         table = self.get_event_table(recurrenceid, "counters")   566    567         if recurrenceid:   568             columns = ["store_user", "object_uid", "object_recurrenceid"]   569             values = [user, uid, recurrenceid]   570         else:   571             columns = ["store_user", "object_uid"]   572             values = [user, uid]   573    574         query, values = self.get_query(   575             "select other from %(table)s :condition" % {   576                 "table" : table   577                 },   578             columns, values)   579    580         self.cursor.execute(query, values)   581         return self.cursor.fetchall()   582    583     def get_counter(self, user, other, uid, recurrenceid=None):   584    585         """   586         For the given 'user', return the counter-proposal from 'other' for the   587         given 'uid' and optional 'recurrenceid'.   588         """   589    590         table = self.get_event_table(recurrenceid, "counters")   591    592         if recurrenceid:   593             columns = ["store_user", "other", "object_uid", "object_recurrenceid"]   594             values = [user, other, uid, recurrenceid]   595         else:   596             columns = ["store_user", "other", "object_uid"]   597             values = [user, other, uid]   598    599         query, values = self.get_query(   600             "select object_text from %(table)s :condition" % {   601                 "table" : table   602                 },   603             columns, values)   604    605         self.cursor.execute(query, values)   606         result = self.cursor.fetchone()   607         return result and parse_string(result[0], "utf-8")   608    609     def set_counter(self, user, other, node, uid, recurrenceid=None):   610    611         """   612         For the given 'user', store a counter-proposal received from 'other' the   613         given 'node' representing that proposal for the given 'uid' and   614         'recurrenceid'.   615         """   616    617         table = self.get_event_table(recurrenceid, "counters")   618    619         if recurrenceid:   620             columns = ["store_user", "other", "object_uid", "object_recurrenceid", "object_text"]   621             values = [user, other, uid, recurrenceid, to_string(node, "utf-8")]   622         else:   623             columns = ["store_user", "other", "object_uid", "object_text"]   624             values = [user, other, uid, to_string(node, "utf-8")]   625    626         query, values = self.get_query(   627             "insert into %(table)s (:columns) values (:values)" % {   628                 "table" : table   629                 },   630             columns, values)   631    632         self.cursor.execute(query, values)   633         return True   634    635     def remove_counters(self, user, uid, recurrenceid=None):   636    637         """   638         For the given 'user', remove all counter-proposals associated with the   639         given 'uid' and 'recurrenceid'.   640         """   641    642         table = self.get_event_table(recurrenceid, "counters")   643    644         if recurrenceid:   645             columns = ["store_user", "object_uid", "object_recurrenceid"]   646             values = [user, uid, recurrenceid]   647         else:   648             columns = ["store_user", "object_uid"]   649             values = [user, uid]   650    651         query, values = self.get_query(   652             "delete from %(table)s :condition" % {   653                 "table" : table   654                 },   655             columns, values)   656    657         self.cursor.execute(query, values)   658         return True   659    660     def remove_counter(self, user, other, uid, recurrenceid=None):   661    662         """   663         For the given 'user', remove any counter-proposal from 'other'   664         associated with the given 'uid' and 'recurrenceid'.   665         """   666    667         table = self.get_event_table(recurrenceid, "counters")   668    669         if recurrenceid:   670             columns = ["store_user", "other", "object_uid", "object_recurrenceid"]   671             values = [user, other, uid, recurrenceid]   672         else:   673             columns = ["store_user", "other", "object_uid"]   674             values = [user, other, uid]   675    676         query, values = self.get_query(   677             "delete from %(table)s :condition" % {   678                 "table" : table   679                 },   680             columns, values)   681    682         self.cursor.execute(query, values)   683         return True   684    685     # Event cancellation.   686    687     def cancel_event(self, user, uid, recurrenceid=None):   688    689         """   690         Cancel an event for 'user' having the given 'uid'. If the optional   691         'recurrenceid' is specified, a specific instance or occurrence of an   692         event is cancelled.   693         """   694    695         table = self.get_event_table(recurrenceid)   696    697         if recurrenceid:   698             columns = ["store_user", "object_uid", "object_recurrenceid"]   699             values = [user, uid, recurrenceid]   700         else:   701             columns = ["store_user", "object_uid"]   702             values = [user, uid]   703    704         setcolumns = ["status"]   705         setvalues = ["cancelled"]   706    707         query, values = self.get_query(   708             "update %(table)s :set :condition" % {   709                 "table" : table   710                 },   711             columns, values, setcolumns, setvalues)   712    713         self.cursor.execute(query, values)   714         return True   715    716     def uncancel_event(self, user, uid, recurrenceid=None):   717    718         """   719         Uncancel an event for 'user' having the given 'uid'. If the optional   720         'recurrenceid' is specified, a specific instance or occurrence of an   721         event is uncancelled.   722         """   723    724         table = self.get_event_table(recurrenceid)   725    726         if recurrenceid:   727             columns = ["store_user", "object_uid", "object_recurrenceid"]   728             values = [user, uid, recurrenceid]   729         else:   730             columns = ["store_user", "object_uid"]   731             values = [user, uid]   732    733         setcolumns = ["status"]   734         setvalues = ["active"]   735    736         query, values = self.get_query(   737             "update %(table)s :set :condition" % {   738                 "table" : table   739                 },   740             columns, values, setcolumns, setvalues)   741    742         self.cursor.execute(query, values)   743         return True   744    745     def remove_cancellation(self, user, uid, recurrenceid=None):   746    747         """   748         Remove a cancellation for 'user' for the event having the given 'uid'.   749         If the optional 'recurrenceid' is specified, a specific instance or   750         occurrence of an event is affected.   751         """   752    753         table = self.get_event_table(recurrenceid)   754    755         if recurrenceid:   756             columns = ["store_user", "object_uid", "object_recurrenceid", "status"]   757             values = [user, uid, recurrenceid, "cancelled"]   758         else:   759             columns = ["store_user", "object_uid", "status"]   760             values = [user, uid, "cancelled"]   761    762         query, values = self.get_query(   763             "delete from %(table)s :condition" % {   764                 "table" : table   765                 },   766             columns, values)   767    768         self.cursor.execute(query, values)   769         return True   770    771 class DatabaseJournal(DatabaseStoreBase, JournalBase):   772    773     "A journal system to support quotas."   774    775     # Quota and user identity/group discovery.   776    777     def get_quotas(self):   778    779         "Return a list of quotas."   780    781         query = "select distinct journal_quota from quota_freebusy"   782         self.cursor.execute(query)   783         return [r[0] for r in self.cursor.fetchall()]   784    785     def get_quota_users(self, quota):   786    787         "Return a list of quota users."   788    789         columns = ["quota"]   790         values = [quota]   791    792         query, values = self.get_query(   793             "select distinct user_group from quota_freebusy :condition",   794             columns, values)   795    796         self.cursor.execute(query)   797         return [r[0] for r in self.cursor.fetchall()]   798    799     # Groups of users sharing quotas.   800    801     def get_groups(self, quota):   802    803         "Return the identity mappings for the given 'quota' as a dictionary."   804    805         columns = ["quota"]   806         values = [quota]   807    808         query, values = self.get_query(   809             "select store_user, user_group from user_groups :condition",   810             columns, values)   811    812         self.cursor.execute(query, values)   813         return dict(self.cursor.fetchall())   814    815     def get_limits(self, quota):   816    817         """   818         Return the limits for the 'quota' as a dictionary mapping identities or   819         groups to durations.   820         """   821    822         columns = ["quota"]   823         values = [quota]   824    825         query, values = self.get_query(   826             "select user_group, quota_limit from quota_limits :condition",   827             columns, values)   828    829         self.cursor.execute(query, values)   830         return dict(self.cursor.fetchall())   831    832     def set_limit(self, quota, group, limit):   833    834         """   835         For the given 'quota', set for a user 'group' the given 'limit' on   836         resource usage.   837         """   838    839         columns = ["quota", "user_group"]   840         values = [quota, group]   841         setcolumns = ["quota_limit"]   842         setvalues = [limit]   843    844         query, values = self.get_query(   845             "update quota_limits :set :condition",   846             columns, values, setcolumns, setvalues)   847    848         self.cursor.execute(query, values)   849    850         if self.cursor.rowcount > 0:   851             return True   852    853         columns = ["quota", "user_group", "quota_limit"]   854         values = [quota, group, limit]   855    856         query, values = self.get_query(   857             "insert into quota_limits (:columns) values (:values)",   858             columns, values)   859    860         self.cursor.execute(query, values)   861         return True   862    863     # Free/busy period access for users within quota groups.   864    865     def get_freebusy(self, quota, user, mutable=False):   866    867         "Get free/busy details for the given 'quota' and 'user'."   868    869         table = "user_freebusy"   870         return FreeBusyDatabaseCollection(self.cursor, table, ["quota", "store_user"], [quota, user], mutable, self.paramstyle)   871    872     def set_freebusy(self, quota, user, freebusy):   873    874         "For the given 'quota' and 'user', set 'freebusy' details."   875    876         table = "user_freebusy"   877    878         if not isinstance(freebusy, FreeBusyDatabaseCollection) or freebusy.table_name != table:   879             fbc = FreeBusyDatabaseCollection(self.cursor, table, ["quota", "store_user"], [quota, user], True, self.paramstyle)   880             fbc += freebusy   881    882         return True   883    884     # Journal entry methods.   885    886     def get_entries(self, quota, group, mutable=False):   887    888         """   889         Return a list of journal entries for the given 'quota' for the indicated   890         'group'.   891         """   892    893         table = "quota_freebusy"   894         return FreeBusyDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], mutable, self.paramstyle)   895    896     def set_entries(self, quota, group, entries):   897    898         """   899         For the given 'quota' and indicated 'group', set the list of journal   900         'entries'.   901         """   902    903         table = "quota_freebusy"   904    905         if not isinstance(entries, FreeBusyDatabaseCollection) or entries.table_name != table:   906             fbc = FreeBusyDatabaseCollection(self.cursor, table, ["quota", "user_group"], [quota, group], True, self.paramstyle)   907             fbc += entries   908    909         return True   910    911 # vim: tabstop=4 expandtab shiftwidth=4