imip-agent

imiptools/stores/database/common.py

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