Changeset 361

Show
Ignore:
Timestamp:
Fri Jan 4 17:51:05 2008
Author:
Brian
Message:

Much refactoring, debugging, and new test script for Share Changes with Server

Files:

Legend:

Unmodified
Added
Removed
Modified
  • server/trunk/update/client/Share Changes with Server.py

    r224 r361  
    1 1 # Commit Changes to Server - write all local changes to server  
    2   # Copyright 2006 by Brian Christensen  
      2 # Copyright 2006, 2007, 2008 by Brian Christensen  
    2 2  
    3 3 #    This file is part of GanttPV.  
     
    37 37 #           if multiple gets use server ids from first get when finding local changes  
    38 38 # 060907 - Alex - fixed display problem that occurred after two users added tasks to the same report  
      39 # 071228 - Brian - refactoring code  
      40 # 080104 - Brian - added code to fix report columns and rows if conflicting changes occur  
      41  
      42 # somewhere I should purge deleted records that haven't been sent to the server  
    39 43  
    40 44 debug = 1  
    41 45  
      46 if 'Data' not in dir():  # this should only happen if the script is loaded as a module by a test script  
      47     import Data  
      48     import wx  
      49     self = None     
      50  
    42 51 # xmlrpclib has some problems with None  
    43 52 def NonesIn(dict):  
     
    56 65             del dict[k]  
    57 66  
      67 def SplitOldNew(record):  
      68     '''Database records contain the current and perhaps the old value of each field  
      69         This routine separates the new and old values  
      70         '''  
      71     old = {}  
      72     new = {}  
      73     for k, v in record.iteritems():  
      74         if k and k[0] == '_':  
      75             old[k[1:]] = v  
      76         else:  
      77             new[k] = v  
      78     return old, new  
      79  
      80 def LocalRowsAdded():  
      81     """Have we added any rows that the server doesn't know about?"""  
      82     count = 0  
      83     old_nextID, new_nextID = SplitOldNew(Data.NextID)  
      84     for k, v in new_nextID.iteritems():  
      85         old = old_nextID.get(k) or 1  
      86         if old < v:  
      87             count += v - old  
      88     return count  
      89  
      90 # this should be returned by a function in Data  
      91 # using 'table/column': table (same format as 'ForeignKey' table)  
      92 # table and id in report rows needs special treatment <- IMPORTANT - To Do  
      93 # (includes all of the non-standard foreign key names)  
      94 foreign_key_xref = {  
      95     'Dependency/PrerequisiteID': 'Task',  
      96     'MeasurementDependency/PriorMeasurementID': 'Measurement',  
      97     'ResourceGrouping/ResourceGroupID': 'Resource',  
      98     'Report/FirstRow': 'ReportRow',  
      99     'ReportRow/NextRow': 'ReportRow',  
      100     'Report/FirstColumn': 'ReportColumn',  
      101     'ReportColumn/NextColumn': 'ReportColumn',  
      102     'ReportType/Also': 'ReportType',  
      103     }  
      104  
      105 def ForeignKeyColumn(tname, cname, row):  # only called if the value is known to be int??  
      106     ''' is this a foreign key column?'''  
      107     tc = '/'.join((tname, cname))  
      108     if tc in foreign_key_xref:  # known and non-standard foreign key column names  
      109         table = foreign_key_xref[tc]  
      110     elif cname[-2:] == 'ID' and cname[:1] != '_':  # follow foreign key naming conventions  
      111         if cname == 'TableID' and tname == 'ReportRow': # report rows are special case  
      112             table = row.get('TableName')  
      113             # if not value or not table: return "" # continue # skip invalid combinations  
      114             if not row.get(cname) or not table: return "" # continue # skip invalid combinations  
      115         else:  # this is the normal case  
      116             table = cname[:-2]  # this might still be an alias (checked later)  
      117     elif tname == 'Report' and cname == 'SelectValue':  # another special case  
      118         selcol = row.get('SelectColumn')  
      119         if selcol and len(selcol) > 2 and selcol[-2:] == 'ID':  
      120             table = selcol[:-2]  # this works for 'ProjectID', what other values can be here?  
      121             # if debug: print "Changed SelectValue %s from %d" % (selcol, value)  
      122             # if debug: print "Changed SelectValue %s from %d" % (selcol, row.get(cname))  
      123         else:  
      124             return ""  # continue  
      125     else:  # not a foreign key  
      126         return ""  # continue  
      127  
      128     table_aliases = Data.Database['TableAlias'][1]  
      129     if table in table_aliases:  
      130         table = table_aliases[table]  
      131     return table  
      132  
      133 def AdjustIDs(old_nextID, new_nextID, new_server_nextid):  
      134     '''  
      135 Diff between old_nextID and new_nextID shows how many rows we added  
      136 Diff between new_nextID and new_server_nextid shows how many rows the server added  
      137 Call this routine only if we have added rows that the server doesn't know about.  
      138 This routine will adjust:  
      139     all of the new row's ids  
      140     all of the foreign keys that point to the new rows  
      141     '''  
      142     # renumber our new rows, if necessary (do we test this? -- not yet)  
      143     # set server version keys to '_'?  -- probably not  
      144     # or keep track of new rows for use FindChanges (BETTER?) -- probably not  
      145     key_adjustment = {} # how much to add to locally assigned IDs  
      146     for tname, v in new_nextID.iteritems():  
      147         key_adjustment[tname] = new_server_nextid.get(tname, 1) - v  
      148  
      149     for tname in new_nextID:  # list of all non-alias table names  
      150         newkeys = {}  
      151         # all IDs in table will be adjusted by the same amount  
      152         row_id_adjustment = key_adjustment.get(tname, 0)  
      153         server_nextid = old_nextID.get(tname, 1)  
      154  
      155         t = Data.Database.get(tname, {})  
      156         for rid, row in t.iteritems():  # process each row in table  
      157             for cname, value in row.iteritems():  # look for foreign key columns  
      158                 if not cname: continue  
      159                 if not isinstance(value, int): continue  # does his work for really big numbers?  
      160                 if cname == 'ID':  
      161                     if value >= server_nextid:  # this is a new row  
      162                         row[cname] = value + row_id_adjustment  
      163                 else:  
      164                     table = ForeignKeyColumn(tname, cname, row)  
      165                     if not table: continue  # ?? is this right  
      166  
      167                     fk_server_nextid = old_nextID.get(table, 1)  
      168                     if value >= fk_server_nextid:  # points to a new row  
      169                         if debug: print "adjusting fk in %s at %s from %d to %d" % (tname, cname, value, value + key_adjustment.get(table, 0))  
      170                         row[cname] = value + key_adjustment.get(table, 0)  
      171             newkeys[row['ID']] = row  
      172         Data.Database[tname] = newkeys  # update tables to use new row ids  
      173  
      174 ##        # recreate aliases  <- this shouldn't be hard coded  
      175 ##        Data.Database['Prerequisite'] = Data.Database['Task']  
      176 ##        if Data.Database.has_key('Resource'):  
      177 ##            Data.Database['ResourceGroup'] = Data.Database['Resource']  
      178 ##        if Data.Database.has_key('Measurement'):  
      179 ##            Data.Database['PriorMeasurement'] = Data.Database['Measurement']  
      180     # recreate aliases  
      181     table_aliases = Data.Database['TableAlias'][1]  
      182     for alias, table in table_aliases.iteritems():  
      183         if table in Data.Database:  # should always be true (except for 'ID')  
      184             Data.Database[alias] = Data.Database[table]  
      185  
      186     Data.PrepDatabase()  # tables have been replaced, fix short cuts  
      187     return  
      188  
      189 # required after AdjustIDs  
      190 def UpdateReportWindowShortcuts():  
      191     for k, v in Data.OpenReports.iteritems():  # update report window short cuts to database  
      192         if k == 1:  
      193             v.UpdatePointers(1)  
      194         else:  
      195             # v.UpdatePointers()  
      196             Data.UpdateDataPointers(v.Report.table, k)  
      197             v.Report.table.UpdateRowPointers()  
      198             v.Report.table.UpdateColumnPointers()  
      199     Data.RefreshReports()  
      200  
      201 def MergeDuplicates(changeids, serverids):  
      202     ''' changeids is a dictionary with key = (table, id), value = new id  
      203         serverids is a dictionary with same format as database['NextID']  
      204             but the server values indicate which records the server knows about  
      205     '''  
      206     # run merge on duplicates???  
      207     # create '__' fields for changes lost?  <-- TO DO  
      208     # when? before or after changing the foreign keys  
      209  
      210     # change foreign keys  
      211     old_nextID, new_nextID = SplitOldNew(Data.NextID)  # just used to get a list of non-alias tables  
      212     for tname in new_nextID:  
      213         t = Data.Database.get(tname, {})  
      214         for rid, row in t.iteritems():  
      215             change = { }  
      216             for cname, value in row.iteritems():  
      217                 if not cname or cname == 'ID': continue  
      218  
      219                 table = ForeignKeyColumn(tname, cname, row)  
      220                 if not table: continue  # ?? is this right  
      221  
      222                 newid = changeids.get((table, value))  # need double prens? - yes  
      223                 if newid:  # pointed to a dup row  
      224                     change[cname] = newid  
      225             if change:  
      226                 if debug: print "changing a foreign key"  
      227                 change['Table'] = tname  
      228                 change['ID'] = rid  
      229                 Data.Update(change, 0)   # We want these changes to be picked up by FindChanges  
      230  
      231     # What do we do with the old records that no one is pointing to? <- To Do  
      232     # we need to delete them or at least change the key fields to  
      233     # None so that they won't be found by mistake. We could clear all  
      234     # 'xxxID' columns and 'Period'.  
      235  
      236     # merge duplicate records  
      237     # give preference to lowest id - should be server copy  
      238     for (table, source), dest in changeids.iteritems():  
      239         t = Data.Database[table]  
      240         for k, v in t[source].iteritems():  
      241             if k.startswith('_') or k == 'ID': continue  
      242             if t[dest].get(k) != v:  # should save varying values even if the old k doesn't exist  
      243                 t[dest]['__' + k] = v  # saving all changed prior values in the record??? - is this the right action??  
      244         # completely clear out the duplicate  
      245         oldnextid = serverids.get('_' + table) or 0 # same format as database['NextID'] table  
      246         if source >= oldnextid:  # delete it if the server doesn't know about it  
      247             if debug: print "deleting the duplicate record in %s with id %s" % (table, source)  
      248             del t[source]  # should test whether local or server? maybe handle them differently?  
      249         else:  # otherwise change all of it's fields to None (normally this shouldn't happen)  
      250             change = {}  
      251             for k in t[source].keys():  
      252                 if k.startswith('_'): continue  # don't change our understanding of the server's values  
      253                 change[k] = None  
      254             change['Table'] = table  
      255             change['ID'] = source  
      256             change['zzStatus'] = 'deleted'  
      257             Data.Update(change, 0)   # We want these changes to be picked up by FindChanges  
      258  
      259 def EditChanges(changes):  
      260     """ Make sure all of the change can be applied before applying any.  
      261     Currently tests the following:  
      262     - Does the specified table exist?  
      263     - Does the table specify an ID number?  
      264  
      265     Changes from the server may include row numbers higher than the numbers we just  
      266     reserved from the server. ??is this right??  
      267     """  
      268     database = Data.Database  # create shortcut  
      269     bad_change = 0  
      270     for batch in changes:  
      271         for c in batch:  
      272             NonesIn(c) # add backing expected Nones  
      273             if debug: print "editting a change"  
      274             table = c.get("Table")  
      275  
      276             # change the test to -- table not in NextID  
      277             # and don't add new tables  
      278             if not table or table in ["NextID"]:  # invalid table names for changes  
      279                 if debug: print "invalid table name", repr(table)  
      280                 bad_change = 1  
      281                 continue  
      282             if not database.has_key(table):  
      283                 # add table to database???  
      284                 Data.AddTable(table)  # I think I have to assume all table names are intended  
      285                                       # we should have all table names from reserved rows  
      286             if debug: print "passed table edit"  
      287  
      288             id = c.get("ID")  
      289             if not id:  
      290                 bad_change = 1  
      291                 if debug: print "failed id edit: no ID"  
      292                 continue  
      293             if not database[table].has_key(id):  
      294                 continue  # new row, don't have to check prior values  
      295             if debug: print "passed id edit"  
      296  
      297             # edit to add:  
      298             # ID shouldn't be one of the new numbers we recently reserved  
      299  
      300             # these edits can be more forgiving now thank's to Alex suggestion of how to  
      301             # handle mismatches between what the client thinks and the servers actual state  
      302             # -- Go over the required changes with Alex  
      303 ##            for k, v in c.items():  # a new list so I can change the old one  
      304 ##                if k in ["Table", "ID"]: continue  
      305 ##                if k[0] == "_": continue  
      306 ##                row = database[table][id]  
      307 ##                 
      308 ##                db_value = row.get("_"+k, row.get(k))  # what we think database has  
      309 ##                prior_value = c.get("_" + k)  
      310 ##                if prior_value != db_value:  # if prior value doesn't match  
      311 ##                    c["__" + k] = db_value    # return real prior value and set error  
      312 ##                    bad_change = 1  
      313 ##                    if debug: print "T:", table, " ID:", id, " Ours: ", db_value, " Theirs:", prior_value  
      314     if debug: print "finished edits"  
      315     return bad_change  
      316      
      317 def ApplyChanges(changes):  
      318     """ Apply changes using method Alex and I discussed  
      319     All of the changes have already been examined to make sure that they can be processed successfully.  
      320     """  
      321     # apply all updates  
      322     database = Data.Database  
      323     for batch in changes:  
      324         for c in batch:  
      325             table = c["Table"]  
      326             id = c["ID"]  
      327             if id not in database[table]:  
      328                 # add row to table  
      329                 # should Data.Update do this for us?  
      330                 database[table][id] = {"ID": id}  
      331                 if database["NextID"][table] <= id:   # this shouldn't happen, but just in case  
      332                     database["NextID"][table] = id + 1  
      333  
      334             row = database[table][id]  # change applies to this row  
      335             row_old, row_new = SplitOldNew(row)  
      336  
      337             # change Data.Update to ignore "_" columns, but for not just apply them???  
      338             # no - just let update apply "_" the same as others??  
      339             # no - update has to know about updates from the database vs. from other local changes  
      340  
      341             c_old, c_new = SplitOldNew(c)  
      342             for k, v in c_new.iteritems(): # check for conflicts  
      343                 if k in ["Table", "ID"]: continue  
      344  
      345                 db_value = row.get(k)  # what we have  
      346                 new_value = v  # c_new.get(k)  # what the server has  
      347                 old_value = c_old.get(k)  # what the server thinks we have  
      348  
      349                 # resolve any conflicts  
      350                 # give priority to the server value but keep a copy of the client value  
      351                 if db_value != new_value:  
      352                     if (k in row_old) or (db_value != old_value):  # did we change the same field?  
      353                         row["__" + k] = db_value  # found a conflict  
      354  
      355                 # use the server's values  
      356                 if new_value or new_value == 0:  
      357                     row[k] = new_value  
      358                 elif k in row:  
      359                     del row[k]  # server value is null; delete old client value  
      360  
      361                 # remove 'changed' flag from updated records  
      362                 if row.has_key("_" + k):  
      363                     del row["_" + k]  
      364  
      365             #Data.Update(c_new, 0)  # should we not use update to apply changes?  
      366                                    # PROBLEM - will the changes from the server be mistaken later  
      367                                    # as changes we made???  
      368                                    # We don't want them to be picked up by FindChanges  
      369  
      370    # Data.SetUndo("Applied Changes from Server")  # To refresh display, but shouldn't allow undo???  <-- TO DO  
      371     # PROBLEM <- if this adds rows? I don't want to add rows until after we send updates to server.  
      372  
      373 # issues: listed here, because this is where we change ids  
      374 #  
      375 # must not commit a change to server that would violate these rules  
      376 # - ReportType and ColumnType - names must be unique  
      377 # - Assignment and Dependency - linked pairs must be unique  
      378 # - All of the date related tables  
      379 #  
      380 # I don't think it's fatal to have these duplicated in the client  
      381 # (I know that duplicate dependency links have been created before)  
      382 # but they should be fixed before committing to the server  
      383 # keep the lowest numbered record, because that will be the one on the server? NOT TRUE!  
      384 #  
      385 # The server should probably edit to prevent updates that cause  
      386 # these problems  
      387 #  
      388 # Table and ID in report rows needs special treatment  <- IMPORTANT - To Do -- ?let update handle??  
      389  
      390 def FindDataProblems():  
      391     ''' Edits performed:  
      392     ReportType.Name must be unique  
      393     (ReportType.Name, ColumnType.Name) must be unique  
      394  
      395     This funtion identifies duplicates and returns an list of the rows that should  
      396     be merged. It doesn't make any changes.  
      397     '''  
      398     changeids = {}  # (table, old id): new id  
      399  
      400     # prevent duplicate names  
      401     def DupNames(table, column):  
      402         index = {}  
      403         t = Data.Database[table]  
      404         rowids = t.keys()  
      405         rowids.sort()  
      406         for k in rowids:  # give precedence to lower row #'s  
      407             name = t[k].get(column)  
      408             if name in index:  
      409                 changeids[(table, k)] = index[name]  
      410             else:  
      411                 index[name] = k  
      412  
      413     DupNames('ReportType', 'Name')  # prevent duplicate report type names  
      414  
      415     # apply de-dup of report types to column types  
      416     # must be done here so column types can be de-dup'ed  
      417     # if debug: print "changeids: ", changeids  
      418     for k, v in Data.Database['ColumnType'].iteritems():  
      419         rtid = v.get('ReportTypeID')  
      420         if ('ReportType', rtid) in changeids:  # it is a dup  
      421             change = {'Table': 'ColumnType', 'ID': k,  
      422                 'ReportTypeID': changeids[('ReportType', rtid)]}  
      423             Data.Update(change, 0)   # We want these changes to be picked up by FindChanges  
      424             if debug: print "should adjust fk in ColumnType at ReportTypeID from %d to %d" % (rtid, changeids[('ReportType', rtid)])  
      425  
      426     # prevent duplicate links  
      427     def DupPair(table, columna, columnb):  
      428         if table not in Data.Database: return  
      429         index = {}  
      430         t = Data.Database[table]  
      431         rowids = t.keys()  
      432         rowids.sort()  
      433         for k in rowids:  # give precedence to lower row #'s  
      434             a = t[k].get(columna)  
      435             b = t[k].get(columnb)  
      436             # should look these up in changeids before checking for duplicates <- IMPORTANT!!!  
      437             if changeids:  
      438                 tablea = ForeignKeyColumn(table, columna, t[k])  
      439                 tableb = ForeignKeyColumn(table, columnb, t[k])  
      440                 if tablea and (tablea, a) in changeids:  
      441                     a = changeids[(tablea, a)]  
      442                 if tableb and (tableb, b) in changeids:  
      443                     b = changeids[(tableb, b)]  
      444             link = ( a, b )  
      445             if link in index:  
      446                 changeids[(table, k)] = index[link]  
      447             else:  
      448                 index[link] = k  
      449  
      450     # specify 'table' and then the columns that as a group must be unique  
      451     # be careful about the order, some of these may identify duplications that  
      452     # a later command will have to know about  
      453     DupPair('Assignment', 'TaskID', 'ResourceID')  
      454     DupPair('Dependency', 'TaskID', 'PrerequisiteID')  
      455     DupPair('ColumnType', 'ReportTypeID', 'Name') # prevent duplicate column names in reporttype  
      456     DupPair('ResourceGrouping', 'ResourceID', 'ResourceGroupID')  # you can only be in the same group once  
      457  
      458     for tablename in Data.Database.keys():  
      459         for period in ('Day', 'Week', 'Month'):  
      460             if tablename.endswith(period):  
      461                 key1 = tablename[:-len(period)] + 'ID'  
      462                 # key2 = period  # this is wrong, should always use 'Period' -- 080101  
      463                 key2 = 'Period'  
      464                 DupPair(tablename, key1, key2)  
      465                 # example 'ProjectDay' becomes 'Project', 'ProjectID', 'Period'  
      466  
      467 ##    # maybe handle reports and columns in a separate method  
      468 ##    # prevent multiple report rows from pointing to the same record  
      469 ##    def DupTrio(table, columna, columnb, columnc):  
      470 ##        index = {}  
      471 ##        t = Data.Database[table]  
      472 ##        rowids = t.keys()  
      473 ##        rowids.sort()  
      474 ##        for k in rowids:  # give precedence to lower row #'s  
      475 ##            link = ( t[k].get(columna), t[k].get(columnb), t[k].get(columnc) )  
      476 ##            if link in index:  
      477 ##                changeids[(table, k)] = index[link]  
      478 ##            else:  
      479 ##                index[link] = k  
      480 ##    DupTrio('ReportRow', 'ReportID', 'TableName', 'TableID')  
      481  
      482     return changeids  
      483          
      484 def FindChanges():  
      485     """ Identify all of the local changes in the client database since last share with server.  
      486         Return them in the format that the server expects to see them.  
      487     """  
      488     changes = [ ]  
      489  
      490 #        tables = Data.Database.items()  
      491 #        tables.sort()  
      492     # for tid, t in tables:  
      493     # for tid, nid in Data.NextID.iteritems():  
      494     old_nextID, new_nextID = SplitOldNew(Data.NextID)  # just used to get a list of non-alias tables  
      495     for tid, nid in new_nextID.iteritems():  
      496         t = Data.Database.get(tid) or {}  
      497         # if tid[0] == "_": continue  # skip internal tables, if any  
      498          
      499         # do the changes need to be sent with the original table name or will aliases work?  
      500         # problems with using aliases:  
      501         #   we don't currently tell the server about aliases we create -- PROBLEM!  
      502         #   the other clients  
      503          
      504 # don't remember why I had this or why it doesn't work now             
      505         server_prior_next = server_prior_nextid.get(tid, 1)  
      506  
      507         # most tables can be handled like this?  
      508         # normal[tid] = t.values()  # list of values (id's are in the records)  
      509         for id, record in t.iteritems():  # each row  
      510             # print "id:", id, " record:", record  
      511             # id = t.get('ID')  # can I assume it is there?  
      512             # if not id: continue  # silently ignore invalid records  
      513  
      514             if id < server_prior_next:  
      515                 old, new = SplitOldNew(record)  
      516                 change = {}  # build a change before adding to changes list  
      517                 for k, v in old.iteritems():  
      518                     if k[0] == '_': continue  # skip saved conflict values  
      519                     change[k] = new.get(k)  
      520                     change["_" + k] = v  
      521  
      522                 if change:  # only include real changes  
      523                     change["Table"] = tid  
      524                     change["ID"] = id  
      525                     NonesOut(change)  
      526                     changes.append(change)  
      527             else:  
      528                 change = record.copy()  
      529                 for k in change.keys():  # omit saved conflict values and "prior server values"  
      530                     if k.startswith('_'):  # these shouldn't be here (but can be created when records are merged)  
      531                         del change[k]  
      532                 change['Table'] = tid  
      533                 NonesOut(change)  
      534                 changes.append(change)  
      535     return changes  
      536  
      537 # Clear server version  
      538 def ClearServerVersions():  
      539     """ If we think we are in sync, clear server version data. """  
      540     old_nextID, new_nextID = SplitOldNew(Data.NextID)  # just used to get a list of non-alias tables  
      541     for tid, nid in new_nextID.iteritems():  # each non-alias table  
      542         t = Data.Database.get(tid) or {}  
      543         for id, record in t.iteritems():  # each row  
      544             for k in record.keys():  # each column  
      545                 if k[0] == '_' and k[:2] != '__':  # delete just the single dash?  
      546                     del record[k]  
      547     return  
      548  
      549 def RememberReports():  
      550     '''Remember rows and columns for any changed reports'''  
      551     report_save = {}  
      552     report = Data.Database['Report']  
      553     column = Data.Database['ReportColumn']  
      554     row = Data.Database['ReportRow']  
      555     for k, v in report.iteritems():  
      556         delta_c = False  
      557         delta_r = False  
      558         cols = Data.GetColumnList(k)  
      559         rows = Data.GetRowList(k)  
      560         if '_FirstColumn' in v:  
      561             delta_c = True  
      562         else:  
      563             for c in cols:  
      564                 if '_NextColumn' in column[c]:  
      565                     delta_c = True  
      566                     break  
      567         if '_FirstRow' in v:  
      568             delta_r = True  
      569         else:  
      570             for r in rows:  
      571                 if '_NextRow' in row[r]:  
      572                     delta_r = True  
      573                     break  
      574         if delta_c:  
      575             report_save[(k, 'column')] = cols  
      576         if delta_r:  
      577             report_save[(k, 'row')] = rows  
      578     return report_save  
      579  
      580 def FixReports(report_save):  
      581     '''If the server changed what we also changed, make sure all of our  
      582         rows/columns still appear in the report  
      583  
      584         Should reconstruct what the server thinks the report looks like  
      585         Follow links but: if underscore use it, unless double underscore  
      586         in which case use non-underscore. Problem is that we don't clear the  
      587         double underscore ever.  
      588     '''  
      589     for key, prior in report_save:  
      590         k, which = key  
      591         if which == 'column':  
      592             cols = Data.GetColumnList(k)  
      593             if prior != cols:  
      594                 Data.ReorderReportColumns(k, prior + cols)  # precedence to local view  
      595         else:  
      596             rows = Data.GetRowList(k)  
      597             if prior != rows:  
      598                 Data.ReorderReportRows(k, prior + rows)  
      599 #    Data.SetUndo('FixReports')  
      600  
    58 601 def DoCall():  
    59 602     import warnings  
     
    70 613      
    71 614     parms = {   # data to make sure were are talking to the right server  
    72           'ScriptVersion': '2006-08-22',  
      615         'ScriptVersion': '2007-01-03',  
    72 615         'FileSignature': Data.Other.get('FileSignature'),  
    73 616         'Key': Data.Other.get('ServerKey'),   # should be saved like file signature  
     
    85 628         return  
    86 629      
    87       # this should be returned by a function in Data  
    88       # using 'table/column': table (same format as 'ForeignKey' table)  
    89       # table and id in report rows needs special treatment <- IMPORTANT - To Do  
    90       foreign_key_xref = {  
    91           'Dependency/PrerequisiteID': 'Task',  
    92           'MeasurementDependency/PriorMeasurementID': 'Measurement',  
    93           'ResourceGrouping/ResourceGroupID': 'Resource',  
    94           'Report/FirstRow': 'ReportRow',  
    95           'ReportRow/NextRow': 'ReportRow',  
    96           'Report/FirstColumn': 'ReportColumn',  
    97           'ReportColumn/NextColumn': 'ReportColumn',  
    98           'ReportType/Also': 'ReportType',  
    99           }  
    100    
    101 630     # server_prior_nextid = {} # set in AdjustIDs, used in FindChanges  
    102 631  
    103       def SplitOldNew(record):  
    104           old = {}  
    105           new = {}  
    106           for k, v in record.iteritems():  
    107               if k and k[0] == '_':  
    108                   old[k[1:]] = v  
    109               else:  
    110                   new[k] = v  
    111           return old, new  
    112    
    113       def LocalRowsAdded():  
    114           """Have we added any rows that the server doesn't know about?"""  
    115           count = 0  
    116           old_nextID, new_nextID = SplitOldNew(Data.NextID)  
    117           for k, v in new_nextID.iteritems():  
    118               old = old_nextID.get(k) or 1  
    119               if old < v:  
    120                   count += v - old  
    121           return count  
    122    
      632     save_reports = RememberReports()  
      633      
    123 634     def GetChangesFromServer():  
    124 635         """ Rows added by client need to be assigned new row numbers from the server.  
     
    152 663             # server's new nextid less what we asked them to reserve  
    153 664             server_prior_nextid[tname] = v - (rows_needed.get(tname) or 0)  
    154                
    155           # renumber our new rows, if necessary (do we test this? -- not yet)  
    156           # set server version keys to '_'?  -- probably not  
    157           # or keep track of new rows for use FindChanges (BETTER?) -- probably not  
    158           key_adjustment = {} # how much to add to locally assigned IDs  
    159           for tname, v in new_nextID.iteritems():  
    160               key_adjustment[tname] = new_server_nextid.get(tname, 1) - v  
    161    
    162           for tname in new_nextID:  # list of all non-alias table names  
    163               newkeys = {}  
    164               # all IDs in table will be adjusted by the same amount  
    165               row_id_adjustment = key_adjustment.get(tname, 0)  
    166               server_nextid = old_nextID.get(tname, 1)  
    167    
    168               t = Data.Database.get(tname, {})  
    169               for rid, row in t.iteritems():  # process each row in table  
    170                   for cname, value in row.iteritems():  # look for foreign key columns  
    171