Package vita :: Package modules :: Package s3 :: Module s3track
[hide private]
[frames] | no frames]

Source Code for Module vita.modules.s3.s3track

  1  # -*- coding: utf-8 -*- 
  2   
  3  """ Simple Generic Location Tracking System 
  4   
  5      @author: Dominic König <dominic[at]aidiq.com> 
  6   
  7      @copyright: 2011 (c) Sahana Software Foundation 
  8      @license: MIT 
  9   
 10      Permission is hereby granted, free of charge, to any person 
 11      obtaining a copy of this software and associated documentation 
 12      files (the "Software"), to deal in the Software without 
 13      restriction, including without limitation the rights to use, 
 14      copy, modify, merge, publish, distribute, sublicense, and/or sell 
 15      copies of the Software, and to permit persons to whom the 
 16      Software is furnished to do so, subject to the following 
 17      conditions: 
 18   
 19      The above copyright notice and this permission notice shall be 
 20      included in all copies or substantial portions of the Software. 
 21   
 22      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 23      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 24      OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 25      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 26      HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 27      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 28      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
 29      OTHER DEALINGS IN THE SOFTWARE. 
 30   
 31  """ 
 32   
 33  from gluon.dal import Table, Query, Set, Expression, Rows, Row 
 34  from datetime import datetime, timedelta 
 35   
 36  __all__ = ["S3Tracking"] 
37 38 # ============================================================================= 39 -class S3Trackable(object):
40 """ 41 Trackable types instance(s) 42 """ 43 44 UID = "uuid" # field name for UIDs 45 46 TRACK_ID = "track_id" # field name for track ID 47 LOCATION_ID = "location_id" # field name for base location 48 49 LOCATION = "gis_location" # location tablename 50 PRESENCE = "sit_presence" # presence tablename 51
52 - def __init__(self, db, trackable, record_id=None, uid=None):
53 """ 54 Constructor: 55 56 @param db: the database (DAL) 57 @param trackable: the trackable object 58 @param record_id: the record ID(s) (if object is a table or tablename) 59 @param uid: the record UID(s) (if object is a table or tablename) 60 """ 61 62 self.db = db 63 self.records = [] 64 65 self.table = db.sit_trackable 66 67 if isinstance(trackable, (Table, str)): 68 if hasattr(trackable, "_tablename"): 69 table = trackable 70 tablename = table._tablename 71 else: 72 table = db[trackable] 73 tablename = trackable 74 fields = self.__get_fields(table) 75 if not fields: 76 raise SyntaxError("No trackable type: %s" % tablename) 77 query = (table._id > 0) 78 if uid is None: 79 if record_id is not None: 80 if isinstance(record_id, (list, tuple)): 81 query = (table._id.belongs(record_id)) 82 else: 83 query = (table._id == record_id) 84 elif UID in table.fields: 85 if not isinstance(uid, (list, tuple)): 86 query = (table[UID].belongs(uid)) 87 else: 88 query = (table[UID] == uid) 89 fields = [table[f] for f in fields] 90 rows = self.db(query).select(*fields) 91 92 elif isinstance(trackable, Row): 93 fields = self.__get_fields(trackable) 94 if not fields: 95 raise SyntaxError("Required fields not present in the row") 96 rows = Rows(records=[trackable], compact=False) 97 98 elif isinstance(trackable, Rows): 99 rows = [r for r in trackable if self.__get_fields(r)] 100 fail = len(trackable) - len(rows) 101 if fail: 102 raise SyntaxError("Required fields not present in %d of the rows" % fail) 103 rows = Rows(records=rows, compact=False) 104 105 elif isinstance(trackable, (Query, Expression)): 106 tablename = self.db._adapter.get_table(trackable) 107 table = self.db[tablename] 108 fields = self.__get_fields(table) 109 if not fields: 110 raise SyntaxError("No trackable type: %s" % tablename) 111 query = trackable 112 fields = [table[f] for f in fields] 113 rows = self.db(query).select(*fields) 114 115 elif isinstance(trackable, Set): 116 query = trackable.query 117 tablename = self.db._adapter.get_table(query) 118 table = self.db[tablename] 119 fields = self.__get_fields(table) 120 if not fields: 121 raise SyntaxError("No trackable type: %s" % tablename) 122 fields = [table[f] for f in fields] 123 rows = trackable.select(*fields) 124 125 else: 126 raise SyntaxError("Invalid parameter type %s" % type(trackable)) 127 128 records = [] 129 for r in rows: 130 if self.__super_entity(r): 131 table = self.db[r.instance_type] 132 fields = self.__get_fields(table, super_entity=False) 133 if not fields: 134 raise SyntaxError("No trackable type: %s" % table._tablename) 135 fields = [table[f] for f in fields] 136 query = table[self.UID] == r[self.UID] 137 row = self.db(query).select(limitby=(0, 1), *fields).first() 138 if row: 139 records.append(row) 140 else: 141 records.append(r) 142 143 self.records = Rows(records=records, compact=False)
144 145 146 # ------------------------------------------------------------------------- 147 @classmethod
148 - def __super_entity(cls, trackable):
149 """ 150 Check whether a trackable is a super-entity 151 152 @param trackable: the trackable object 153 """ 154 155 if hasattr(trackable, "fields"): 156 keys = trackable.fields 157 else: 158 keys = trackable 159 160 return "instance_type" in keys
161 162 163 # ------------------------------------------------------------------------- 164 @classmethod
165 - def __get_fields(cls, trackable, super_entity=True):
166 """ 167 Check a trackable for presence of required fields 168 169 @param: the trackable object 170 """ 171 172 fields = [] 173 174 if hasattr(trackable, "fields"): 175 keys = trackable.fields 176 else: 177 keys = trackable 178 179 try: 180 if super_entity and \ 181 cls.__super_entity(trackable) and cls.UID in keys: 182 return ("instance_type", cls.UID) 183 if cls.LOCATION_ID in keys: 184 fields.append(cls.LOCATION_ID) 185 if cls.TRACK_ID in keys: 186 fields.append(cls.TRACK_ID) 187 return fields 188 elif hasattr(trackable, "update_record") or \ 189 isinstance(trackable, Table): 190 return fields 191 except: 192 pass 193 return None
194 195 196 # -------------------------------------------------------------------------
197 - def get_location(self, timestmp=None, 198 _fields=None, 199 _filter=None, 200 as_rows=False, 201 exclude=[]):
202 """ 203 Get the current location of the instance(s) (at the given time) 204 205 @param timestmp: last datetime for presence (defaults to current time) 206 @param _fields: fields to retrieve from the location records (None for ALL) 207 @param _filter: filter for the locations 208 @param as_rows: return the result as Rows object 209 @param exclude: interlocks to break at (avoids circular check-ins) 210 211 @returns: a location record, or a list of location records (if multiple) 212 """ 213 214 ptable = self.db[self.PRESENCE] 215 ltable = self.db[self.LOCATION] 216 217 if timestmp is None: 218 timestmp = datetime.utcnow() 219 220 locations = [] 221 for r in self.records: 222 location = None 223 if self.TRACK_ID in r: 224 query = ((ptable.deleted == False) & 225 (ptable[self.TRACK_ID] == r[self.TRACK_ID]) & 226 (ptable.timestmp <= timestmp)) 227 presence = self.db(query).select(orderby=~ptable.timestmp, 228 limitby=(0, 1)).first() 229 if presence: 230 if presence.interlock: 231 exclude = [r[self.TRACK_ID]] + exclude 232 tablename, record = presence.interlock.split(",", 1) 233 trackable = S3Trackable(self.db, tablename, record) 234 record = trackable.records.first() 235 if self.TRACK_ID not in record or \ 236 record[self.TRACK_ID] not in exclude: 237 location = trackable.get_location(timestmp=timestmp, 238 exclude=exclude) 239 elif presence.location_id: 240 query = (ltable.id == presence.location_id) 241 if _filter is not None: 242 query = query & _filter 243 if _fields is None: 244 location = self.db(query).select(ltable.ALL, 245 limitby=(0, 1)).first() 246 else: 247 location = self.db(query).select(limitby=(0, 1), 248 *_fields).first() 249 250 if not location: 251 if len(self.records) > 1: 252 trackable = S3Trackable(self.db, r) 253 else: 254 trackable = self 255 location = trackable.get_base_location() 256 257 if location: 258 locations.append(location) 259 260 if as_rows: 261 return Rows(records=locations, compact=False) 262 263 if not locations: 264 return None 265 elif len(locations) == 1: 266 return locations[0] 267 else: 268 return locations
269 270 271 # -------------------------------------------------------------------------
272 - def set_location(self, location, timestmp=None):
273 """ 274 Set the current location of instance(s) (at the given time) 275 276 @param location: the location (as Row or record ID) 277 @param timestmp: the datetime of the presence (defaults to current time) 278 279 @returns: nothing 280 """ 281 282 ptable = self.db[self.PRESENCE] 283 284 if timestmp is None: 285 timestmp = datetime.utcnow() 286 287 if isinstance(location, S3Trackable): 288 location = location.get_base_location() 289 if isinstance(location, Rows): 290 location = location.first() 291 if isinstance(location, Row): 292 if "location_id" in location: 293 location = location.location_id 294 else: 295 location = location.id 296 297 if not location: 298 return 299 else: 300 data = dict(location_id=location, timestmp=timestmp) 301 302 for r in self.records: 303 if self.TRACK_ID not in r: 304 # No track ID => set base location 305 if len(self.records) > 1: 306 trackable = S3Trackable(r) 307 else: 308 trackable = self 309 trackable.set_base_location(location) 310 elif r[self.TRACK_ID]: 311 data.update({self.TRACK_ID:r[self.TRACK_ID]}) 312 ptable.insert(**data) 313 self.__update_timestamp(r[self.TRACK_ID], timestmp)
314 315 316 # -------------------------------------------------------------------------
317 - def check_in(self, table, record, timestmp=None):
318 """ 319 Bind the presence of the instance(s) to another instance 320 321 @param table: table name of the other resource 322 @param record: record in the other resource (as Row or record ID) 323 @param timestmp: datetime of the check-in 324 325 @returns: nothing 326 """ 327 328 ptable = self.db[self.PRESENCE] 329 330 if isinstance(table, str): 331 table = self.db[table] 332 fields = self.__get_fields(table) 333 if not fields: 334 raise SyntaxError("No location data in %s" % table._tablename) 335 336 interlock = None 337 if isinstance(record, Rows): 338 record = record.first() 339 if not isinstance(record, Row): 340 record = table[record] 341 if self.__super_entity(record): 342 table = self.db[record.instance_type] 343 fields = self.__get_fields(table, super_entity=False) 344 if not fields: 345 raise SyntaxError("No trackable type: %s" % table._tablename) 346 query = table[self.UID] == record[self.UID] 347 record = self.db(query).select(limitby=(0, 1)).first() 348 if record and table._id.name in record: 349 record = record[table._id.name] 350 if record: 351 interlock = "%s,%s" % (table, record) 352 else: 353 raise SyntaxError("No record specified for %s" % table._tablename) 354 355 if interlock: 356 if timestmp is None: 357 timestmp = datetime.utcnow() 358 data = dict(location_id=None, 359 timestmp=timestmp, 360 interlock=interlock) 361 q = ((ptable.deleted == False) & (ptable.timestmp <= timestmp)) 362 for r in self.records: 363 if self.TRACK_ID not in r: 364 # Cannot check-in a non-trackable 365 continue 366 query = q & (ptable[self.TRACK_ID] == r[self.TRACK_ID]) 367 presence = self.db(query).select(orderby=~ptable.timestmp, 368 limitby=(0, 1)).first() 369 if presence and presence.interlock == interlock: 370 # already checked-in to the same instance 371 continue 372 data.update({self.TRACK_ID:r[self.TRACK_ID]}) 373 ptable.insert(**data) 374 self.__update_timestamp(r[self.TRACK_ID], timestmp)
375 376 377 # -------------------------------------------------------------------------
378 - def check_out(self, table=None, record=None, timestmp=None):
379 """ 380 Make the last log entry before timestmp independent from 381 the referenced entity (if any) 382 383 @param timestmp: the date/time of the check-out, defaults 384 to current time 385 386 """ 387 388 ptable = self.db[self.PRESENCE] 389 390 if timestmp is None: 391 timestmp = datetime.utcnow() 392 393 interlock = None 394 if table is not None: 395 if isinstance(table, str): 396 table = self.db[table] 397 if isinstance(record, Rows): 398 record = record.first() 399 if self.__super_entity(table): 400 if not isinstance(record, Row): 401 record = table[record] 402 table = self.db[record.instance_type] 403 fields = self.__get_fields(table, super_entity=False) 404 if not fields: 405 raise SyntaxError("No trackable type: %s" % table._tablename) 406 query = table[self.UID] == record[self.UID] 407 record = self.db(query).select(limitby=(0, 1)).first() 408 if isinstance(record, Row) and table._id.name in record: 409 record = record[table._id.name] 410 if record: 411 interlock = "%s,%s" % (table, record) 412 else: 413 return 414 415 q = ((ptable.deleted == False) & (ptable.timestmp <= timestmp)) 416 417 for r in self.records: 418 if self.TRACK_ID not in r: 419 # Cannot check-out a non-trackable 420 continue 421 query = q & (ptable[self.TRACK_ID] == r[self.TRACK_ID]) 422 presence = self.db(query).select(orderby=~ptable.timestmp, 423 limitby=(0, 1)).first() 424 if presence and presence.interlock: 425 if interlock and presence.interlock != interlock: 426 continue 427 elif not interlock and table and \ 428 not presence.interlock.startswith("%s" % table): 429 continue 430 tablename, record = presence.interlock.split(",", 1) 431 trackable = S3Trackable(self.db, tablename, record) 432 location = trackable.get_location(timestmp=timestmp) 433 if timestmp - presence.timestmp < timedelta(seconds=1): 434 timestmp = timestmp + timedelta(seconds=1) 435 data = dict(location_id=location, 436 timestmp=timestmp, 437 interlock=None) 438 data.update({self.TRACK_ID:r[self.TRACK_ID]}) 439 ptable.insert(**data) 440 self.__update_timestamp(r[self.TRACK_ID], timestmp)
441 442 443 # -------------------------------------------------------------------------
444 - def remove_location(self, location=None):
445 """ 446 Remove a location from the presence log of the instance(s) 447 448 @todo: implement 449 """ 450 raise NotImplementedError
451 452 453 # -------------------------------------------------------------------------
454 - def get_base_location(self, 455 _fields=None, 456 _filter=None, 457 as_rows=False):
458 """ 459 Get the base location of the instance(s) 460 461 @param _fields: fields to retrieve from the location records (None for ALL) 462 @param _filter: filter for the locations 463 @param as_rows: return the result as Rows object 464 465 @returns: the base location(s) of the current instance 466 """ 467 468 ltable = self.db[self.LOCATION] 469 470 locations = [] 471 for r in self.records: 472 location = None 473 query = None 474 if self.LOCATION_ID in r: 475 query = (ltable.id == r[self.LOCATION_ID]) 476 elif self.TRACK_ID in r: 477 q = (self.table[self.TRACK_ID] == r[self.TRACK_ID]) 478 trackable = self.db(q).select(limitby=(0, 1)).first() 479 table = self.db[trackable.instance_type] 480 if self.LOCATION_ID in table.fields: 481 query = ((table[self.TRACK_ID] == r[self.TRACK_ID]) & 482 (table[self.LOCATION_ID] == ltable.id)) 483 if query: 484 if _filter is not None: 485 query = query & _filter 486 if not _fields: 487 location = self.db(query).select(ltable.ALL, 488 limitby=(0, 1)).first() 489 else: 490 location = self.db(query).select(limitby=(0, 1), 491 *_fields).first() 492 if location: 493 locations.append(location) 494 495 if as_rows: 496 return Rows(records=locations, compact=False) 497 498 if not locations: 499 return None 500 elif len(locations) == 1: 501 return locations[0] 502 else: 503 return locations
504 505 506 # -------------------------------------------------------------------------
507 - def set_base_location(self, location=None):
508 """ 509 Set the base location of the instance(s) 510 511 @param location: the location for the base location as Row or record ID 512 513 @returns: nothing 514 515 @note: instance tables without a location_id field will be ignored 516 """ 517 518 if isinstance(location, S3Trackable): 519 location = location.get_base_location() 520 if isinstance(location, Rows): 521 location = location.first() 522 if isinstance(location, Row): 523 location = location.id 524 525 if not location: 526 return 527 else: 528 data = {self.LOCATION_ID:location} 529 530 # Update records without track ID 531 for r in self.records: 532 if self.TRACK_ID in r: 533 continue 534 elif self.LOCATION_ID in r and hasattr(r, "update_record"): 535 r.update_record(**data) 536 537 # Update records with track ID 538 # => this can happen table-wise = less queries 539 track_ids = [r[self.TRACK_ID] for r in self.records 540 if self.TRACK_ID in r] 541 rows = self.db(self.table[self.TRACK_ID].belongs(track_ids)).select() 542 tables = [] 543 for r in rows: 544 instance_type = r.instance_type 545 table = self.db[instance_type] 546 if instance_type not in tables and \ 547 self.LOCATION_ID in table.fields: 548 tables.append(table) 549 else: 550 # No location ID in this type => ignore gracefully 551 continue 552 553 # Location specified => update all base locations 554 for table in tables: 555 self.db(table[self.TRACK_ID].belongs(track_ids)).update(**data) 556 557 # Refresh records 558 for r in self.records: 559 if self.LOCATION_ID in r: 560 r[self.LOCATION_ID] = location
561 562 563 # -------------------------------------------------------------------------
564 - def __update_timestamp(self, track_id, timestamp):
565 """ 566 Update the timestamp of a trackable 567 568 @param track_id: the trackable ID (super-entity key) 569 @param timestamp: the timestamp 570 """ 571 572 if timestamp is None: 573 timestamp = datetime.utcnow() 574 if track_id: 575 trackable = self.table[track_id] 576 if trackable: 577 trackable.update_record(track_timestmp=timestamp)
578
579 580 # ============================================================================= 581 -class S3Tracking(object):
582 """ 583 S3 Tracking system, to be instantiated once as global "s3_trackable" object 584 585 """ 586
587 - def __init__(self, env):
588 """ 589 Constructor 590 591 @param env: the global environment (globals()) 592 593 """ 594 595 self.db = env["db"]
596 597 598 # -------------------------------------------------------------------------
599 - def __call__(self, trackable, record_id=None, uid=None):
600 """ 601 Get a tracking interface for a record or set of records 602 603 @param trackable: a Row, Rows, Query, Expression, Set object or 604 a Table or a tablename 605 @param record_id: a record ID or a list/tuple of record IDs (together 606 with Table or tablename) 607 @param uid: a record UID or a list/tuple of record UIDs (together with 608 Table or tablename) 609 610 @returns: a S3Trackable instance for the specified record(s) 611 612 """ 613 614 return S3Trackable(self.db, trackable, 615 record_id=record_id, 616 uid=uid)
617 618 619 # -------------------------------------------------------------------------
620 - def get_all(self, entity, 621 location=None, 622 bbox=None, 623 timestmp=None):
624 """ 625 Get all instances of the given entity at the given location and time 626 627 """ 628 raise NotImplementedError
629 630 631 # -------------------------------------------------------------------------
632 - def get_checked_in(self, table, record, 633 instance_type=None, 634 timestmp=None):
635 """ 636 Get all trackables of the given type that are checked-in 637 to the given instance at the given time 638 639 """ 640 raise NotImplementedError
641 642 643 # ============================================================================= 644