1
2
3 """ Simple Generic Location Tracking System
4
5 @copyright: 2011-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
28
29 """
30
31 from datetime import datetime, timedelta
32
33 from gluon import current, HTTP, FORM, INPUT, LABEL, TABLE
34 from gluon.storage import Storage
35
36 from s3dal import Table, Rows, Row
37 from s3rest import S3Method
38
39 __all__ = ("S3Trackable",
40 "S3Tracker",
41 "S3CheckInMethod",
42 "S3CheckOutMethod",
43 )
44
45 UID = "uuid"
46
47 TRACK_ID = "track_id"
48 LOCATION_ID = "location_id"
49
50 LOCATION = "gis_location"
51 PRESENCE = "sit_presence"
55 """
56 Trackable types instance(s)
57 """
58
59 - def __init__(self, table=None, tablename=None, record=None, query=None,
60 record_id=None, record_ids=None, rtable=None):
61 """
62 Constructor:
63
64 @param table: a Table object
65 @param tablename: a Str tablename
66 @param record: a Row object
67 @param query: a Query object
68 @param record_id: a record ID (if object is a Table)
69 @param record_ids: a list of record IDs (if object is a Table)
70 - these should be in ascending order
71 @param rtable: the resource table (for the recursive calls)
72 """
73
74 db = current.db
75 s3db = current.s3db
76
77 self.records = []
78
79 self.table = s3db.sit_trackable
80 self.rtable = rtable
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106 if table or tablename:
107 if table:
108 tablename = table._tablename
109 else:
110 table = s3db[tablename]
111 fields = self.__get_fields(table)
112 if not fields:
113 raise SyntaxError("Not a trackable type: %s" % tablename)
114 if record_ids:
115 query = (table._id.belongs(record_ids))
116 limitby = (0, len(record_ids))
117 orderby = table._id
118 elif record_id:
119 query = (table._id == record_id)
120 limitby = (0, 1)
121 orderby = None
122 else:
123 query = (table._id > 0)
124 limitby = None
125 orderby = table._id
126 fields = [table[f] for f in fields]
127 rows = db(query).select(limitby=limitby, orderby=orderby, *fields)
128
129
130
131
132
133
134 elif record:
135 fields = self.__get_fields(record)
136 if not fields:
137 raise SyntaxError("Required fields not present in the row")
138 rows = Rows(records=[record], compact=False)
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156 elif query:
157 tablename = db._adapter.get_table(query)
158 self.rtable = s3db[tablename]
159 fields = self.__get_fields(self.rtable)
160 if not fields:
161 raise SyntaxError("Table %s is not a trackable type" % table._tablename)
162 fields = [self.rtable[f] for f in fields]
163 rows = db(query).select(*fields)
164
165
166
167
168
169
170
171
172
173
174
175 else:
176 raise SyntaxError("Invalid parameters")
177
178 records = []
179 for r in rows:
180 if self.__super_entity(r):
181 table = s3db[r.instance_type]
182 fields = self.__get_fields(table, super_entity=False)
183 if not fields:
184 raise SyntaxError("Table %s is not a trackable type" % table._tablename)
185 fields = [table[f] for f in fields]
186 row = db(table[UID] == r[UID]).select(limitby=(0, 1),
187 *fields).first()
188 if row:
189 records.append(row)
190 else:
191 records.append(r)
192
193 self.records = Rows(records=records, compact=False)
194
195
196 @staticmethod
198 """
199 Check whether a trackable is a super-entity
200
201 @param trackable: the trackable object
202 """
203
204 if hasattr(trackable, "fields"):
205 keys = trackable.fields
206 else:
207 keys = trackable
208
209 return "instance_type" in keys
210
211
212 @classmethod
214 """
215 Check a trackable for presence of required fields
216
217 @param: the trackable object
218 """
219
220 fields = []
221
222 if hasattr(trackable, "fields"):
223 keys = trackable.fields
224 else:
225 keys = trackable
226
227 if super_entity and \
228 cls.__super_entity(trackable) and UID in keys:
229 return ("instance_type", UID)
230 if LOCATION_ID in keys:
231 fields.append(LOCATION_ID)
232 if TRACK_ID in keys:
233 fields.append(TRACK_ID)
234 return fields
235 elif hasattr(trackable, "update_record") or \
236 isinstance(trackable, Table) or \
237 isinstance(trackable, Row):
238 return fields
239
240 return None
241
242
243 - def get_location(self,
244 timestmp=None,
245 _fields=None,
246 _filter=None,
247 as_rows=False,
248 exclude=None,
249 empty = True):
250 """
251 Get the current location of the instance(s) (at the given time)
252
253 @param timestmp: last datetime for presence (defaults to current time)
254 @param _fields: fields to retrieve from the location records (None for ALL)
255 @param _filter: filter for the locations
256 @param as_rows: return the result as Rows object
257 @param exclude: interlocks to break at (avoids circular check-ins)
258 @param empty: return None if no locations (set to False by gis.get_location_data())
259
260 @return: a location record, or a list of location records (if multiple)
261
262 @ToDo: Also show Timestamp of when seen there
263 """
264
265 if exclude is None:
266 exclude = []
267
268 db = current.db
269 s3db = current.s3db
270
271 ptable = s3db[PRESENCE]
272 ltable = s3db[LOCATION]
273
274 if timestmp is None:
275 timestmp = datetime.utcnow()
276
277 locations = []
278 for r in self.records:
279 location = None
280 if TRACK_ID in r:
281 query = ((ptable.deleted == False) & \
282 (ptable[TRACK_ID] == r[TRACK_ID]) & \
283 (ptable.timestmp <= timestmp))
284 presence = db(query).select(orderby=~ptable.timestmp,
285 limitby=(0, 1)).first()
286 if presence:
287 if presence.interlock:
288 exclude = [r[TRACK_ID]] + exclude
289 tablename, record_id = presence.interlock.split(",", 1)
290 trackable = S3Trackable(tablename=tablename, record_id=record_id)
291 record = trackable.records.first()
292 if TRACK_ID not in record or \
293 record[TRACK_ID] not in exclude:
294 location = trackable.get_location(timestmp=timestmp,
295 exclude=exclude,
296 _fields=_fields,
297 as_rows=True).first()
298 elif presence.location_id:
299 query = (ltable.id == presence.location_id)
300 if _filter is not None:
301 query = query & _filter
302 if _fields is None:
303 location = db(query).select(ltable.ALL,
304 limitby=(0, 1)).first()
305 else:
306 location = db(query).select(limitby=(0, 1),
307 *_fields).first()
308
309 if not location:
310 if len(self.records) > 1:
311 trackable = S3Trackable(record=r, rtable=self.rtable)
312 else:
313 trackable = self
314 location = trackable.get_base_location(_fields=_fields)
315
316 if location:
317 locations.append(location)
318 elif not empty:
319
320 locations.append(Row({"lat": None, "lon": None}))
321
322 if as_rows:
323 return Rows(records=locations, compact=False)
324
325 if not locations:
326 return None
327 else:
328 return locations
329
330
332 """
333 Set the current location of instance(s) (at the given time)
334
335 @param location: the location (as Row or record ID)
336 @param timestmp: the datetime of the presence (defaults to current time)
337
338 @return: location
339 """
340
341 ptable = current.s3db[PRESENCE]
342
343 if timestmp is None:
344 timestmp = datetime.utcnow()
345
346 if isinstance(location, S3Trackable):
347 location = location.get_base_location()
348 if isinstance(location, Rows):
349 location = location.first()
350 if isinstance(location, Row):
351 if "location_id" in location:
352 location = location.location_id
353 else:
354 location = location.id
355
356
357
358
359
360 data = dict(location_id=location, timestmp=timestmp)
361
362 for r in self.records:
363 if TRACK_ID not in r:
364
365 if len(self.records) > 1:
366 trackable = S3Trackable(record=r)
367 else:
368 trackable = self
369 trackable.set_base_location(location)
370 elif r[TRACK_ID]:
371 data.update({TRACK_ID:r[TRACK_ID]})
372 ptable.insert(**data)
373 self.__update_timestamp(r[TRACK_ID], timestmp)
374
375 return location
376
377
378 - def check_in(self, table, record, timestmp=None):
379 """
380 Bind the presence of the instance(s) to another instance
381
382 @param table: table name of the other resource
383 @param record: record in the other resource (as Row or record ID)
384 @param timestmp: datetime of the check-in
385
386 @return: nothing
387 """
388
389 db = current.db
390 s3db = current.s3db
391
392 ptable = s3db[PRESENCE]
393
394 if isinstance(table, str):
395 table = s3db[table]
396
397 fields = self.__get_fields(table)
398 if not fields:
399 raise SyntaxError("No location data in %s" % table._tablename)
400
401 interlock = None
402
403 if isinstance(record, Rows):
404 record = record.first()
405
406 if not isinstance(record, Row):
407 if not self.__super_entity(table):
408 fields = (table._id,)
409 record = db(table._id == record).select(limitby=(0, 1), *fields).first()
410
411 if self.__super_entity(record):
412
413
414 table = s3db[record.instance_type]
415 if not self.__get_fields(table, super_entity=False):
416 raise SyntaxError("No trackable type: %s" % table._tablename)
417
418
419 query = (table[UID] == record[UID])
420 record = db(query).select(table._id, limitby=(0, 1), *fields).first()
421
422 try:
423 record_id = record[table._id] if record else None
424 except AttributeError:
425 record_id = None
426 if record_id:
427 interlock = "%s,%s" % (table, record_id)
428 else:
429 raise SyntaxError("No record specified for %s" % table._tablename)
430
431 if interlock:
432
433 if timestmp is None:
434 timestmp = datetime.utcnow()
435
436 data = {"location_id": None,
437 "timestmp": timestmp,
438 "interlock": interlock,
439 }
440
441 q = (ptable.timestmp <= timestmp) & \
442 (ptable.deleted == False)
443 for r in self.records:
444
445 if TRACK_ID not in r:
446
447 continue
448 track_id = r[TRACK_ID]
449
450 query = (ptable[TRACK_ID] == track_id) & q
451 presence = db(query).select(ptable.interlock,
452 orderby = ~ptable.timestmp,
453 limitby = (0, 1),
454 ).first()
455 if presence and presence.interlock == interlock:
456
457 continue
458 data[TRACK_ID] = track_id
459
460 ptable.insert(**data)
461 self.__update_timestamp(track_id, timestmp)
462
463
464 - def check_out(self, table=None, record=None, timestmp=None):
465 """
466 Make the last log entry before timestmp independent from
467 the referenced entity (if any)
468
469 @param timestmp: the date/time of the check-out, defaults
470 to current time
471 """
472
473 db = current.db
474 s3db = current.s3db
475
476 ptable = s3db[PRESENCE]
477
478 if timestmp is None:
479 timestmp = datetime.utcnow()
480
481 interlock = None
482 if table is not None:
483 if isinstance(table, str):
484 table = s3db[table]
485 if isinstance(record, Rows):
486 record = record.first()
487 if self.__super_entity(table):
488 if not isinstance(record, Row):
489 record = table[record]
490 table = s3db[record.instance_type]
491 fields = self.__get_fields(table, super_entity=False)
492 if not fields:
493 raise SyntaxError("No trackable type: %s" % table._tablename)
494 query = table[UID] == record[UID]
495 record = db(query).select(limitby=(0, 1)).first()
496 if isinstance(record, Row) and table._id.name in record:
497 record = record[table._id.name]
498 if record:
499 interlock = "%s,%s" % (table, record)
500 else:
501 return
502
503 q = ((ptable.deleted == False) & (ptable.timestmp <= timestmp))
504
505 for r in self.records:
506 if TRACK_ID not in r:
507
508 continue
509 query = q & (ptable[TRACK_ID] == r[TRACK_ID])
510 presence = db(query).select(orderby=~ptable.timestmp,
511 limitby=(0, 1)).first()
512 if presence and presence.interlock:
513 if interlock and presence.interlock != interlock:
514 continue
515 elif not interlock and table and \
516 not presence.interlock.startswith("%s" % table):
517 continue
518 tablename, record_id = presence.interlock.split(",", 1)
519 trackable = S3Trackable(tablename=tablename, record_id=record_id)
520 location = trackable.get_location(_fields=["id"],
521 timestmp=timestmp,
522 as_rows=True).first()
523 if timestmp - presence.timestmp < timedelta(seconds=1):
524 timestmp = timestmp + timedelta(seconds=1)
525 data = dict(location_id=location.id,
526 timestmp=timestmp,
527 interlock=None)
528 data.update({TRACK_ID:r[TRACK_ID]})
529 ptable.insert(**data)
530 self.__update_timestamp(r[TRACK_ID], timestmp)
531
532
534 """
535 Remove a location from the presence log of the instance(s)
536
537 @todo: implement
538 """
539 raise NotImplementedError
540
541
542 - def get_base_location(self,
543 _fields=None,
544 _filter=None,
545 as_rows=False,
546 empty=True):
547 """
548 Get the base location of the instance(s)
549
550 @param _fields: fields to retrieve from the location records (None for ALL)
551 @param _filter: filter for the locations
552 @param as_rows: return the result as Rows object
553 @param empty: return None if no locations (set to False by gis.get_location_data())
554
555 @return: the base location(s) of the current instance
556 """
557
558 db = current.db
559 s3db = current.s3db
560
561 ltable = s3db[LOCATION]
562 rtable = self.rtable
563
564 locations = []
565 for r in self.records:
566 location = None
567 query = None
568 if LOCATION_ID in r:
569 query = (ltable.id == r[LOCATION_ID])
570 if rtable:
571 query = query & (rtable[LOCATION_ID] == ltable.id)
572 if TRACK_ID in r:
573 query = query & (rtable[TRACK_ID] == r[TRACK_ID])
574 elif TRACK_ID in r:
575 q = (self.table[TRACK_ID] == r[TRACK_ID])
576 trackable = db(q).select(limitby=(0, 1)).first()
577 table = s3db[trackable.instance_type]
578 if LOCATION_ID in table.fields:
579 query = ((table[TRACK_ID] == r[TRACK_ID]) &
580 (table[LOCATION_ID] == ltable.id))
581 if query:
582 if _filter is not None:
583 query = query & _filter
584 if not _fields:
585 location = db(query).select(ltable.ALL,
586 limitby=(0, 1)).first()
587 else:
588 location = db(query).select(limitby=(0, 1),
589 *_fields).first()
590 if location:
591 locations.append(location)
592 elif not empty:
593
594 locations.append(Row({"lat": None, "lon": None}))
595
596 if as_rows:
597 return Rows(records=locations, compact=False)
598
599 if not locations:
600 return None
601 elif len(locations) == 1:
602 return locations[0]
603 else:
604 return locations
605
606
608 """
609 Set the base location of the instance(s)
610
611 @param location: the location for the base location as Row or record ID
612
613 @return: nothing
614
615 @note: instance tables without a location_id field will be ignored
616 """
617
618 if isinstance(location, S3Trackable):
619 location = location.get_base_location()
620 if isinstance(location, Rows):
621 location = location.first()
622 if isinstance(location, Row):
623 location.get("id", None)
624
625 if not location or not str(location).isdigit():
626
627 return
628 else:
629 data = {LOCATION_ID:location}
630
631
632 for r in self.records:
633 if TRACK_ID in r:
634 continue
635 elif LOCATION_ID in r:
636 if hasattr(r, "update_record"):
637 r.update_record(**data)
638 else:
639 raise SyntaxError("Cannot relate record to a table.")
640
641 db = current.db
642 s3db = current.s3db
643
644
645
646 track_ids = [r[TRACK_ID] for r in self.records if TRACK_ID in r]
647 rows = db(self.table[TRACK_ID].belongs(track_ids)).select()
648
649 tables = []
650 append = tables.append
651 types = set()
652 seen = types.add
653 for r in rows:
654 instance_type = r.instance_type
655 if instance_type not in types:
656 seen(instance_type)
657 table = s3db[instance_type]
658 if instance_type not in tables and LOCATION_ID in table.fields:
659 append(table)
660 else:
661
662 continue
663
664
665 for table in tables:
666 db(table[TRACK_ID].belongs(track_ids)).update(**data)
667
668
669 for r in self.records:
670 if LOCATION_ID in r:
671 r[LOCATION_ID] = location
672
673 return location
674
675
677 """
678 Update the timestamp of a trackable
679
680 @param track_id: the trackable ID (super-entity key)
681 @param timestamp: the timestamp
682 """
683
684 if track_id:
685 if timestamp is None:
686 timestamp = datetime.utcnow()
687 current.db(self.table.track_id == track_id).update(track_timestmp=timestamp)
688
691 """
692 S3 Tracking system, can be instantiated once as global 's3tracker' object
693 """
694
696 """
697 Constructor
698 """
699
700
701 - def __call__(self, table=None, record_id=None, record_ids=None,
702 tablename=None, record=None, query=None):
703 """
704 Get a tracking interface for a record or set of records
705
706 @param table: a Table object
707 @param record_id: a record ID (together with Table or tablename)
708 @param record_ids: a list/tuple of record IDs (together with Table or tablename)
709 @param tablename: a Str object
710 @param record: a Row object
711 @param query: a Query object
712
713 @return: a S3Trackable instance for the specified record(s)
714 """
715
716 return S3Trackable(table=table,
717 tablename=tablename,
718 record_id=record_id,
719 record_ids=record_ids,
720 record=record,
721 query=query,
722 )
723
724
725 - def get_all(self, entity,
726 location=None,
727 bbox=None,
728 timestmp=None):
729 """
730 Get all instances of the given entity at the given location and time
731 """
732 raise NotImplementedError
733
734
735 - def get_checked_in(self, table, record,
736 instance_type=None,
737 timestmp=None):
738 """
739 Get all trackables of the given type that are checked-in
740 to the given instance at the given time
741 """
742 raise NotImplementedError
743
746 """
747 Custom Method to allow a trackable resource to check-in
748 """
749
750
751 @staticmethod
753 """
754 Apply method.
755
756 @param r: the S3Request
757 @param attr: controller options for this request
758 """
759
760 if r.representation == "html":
761
762 T = current.T
763 s3db = current.s3db
764 response = current.response
765 table = r.table
766 tracker = S3Trackable(table, record_id=r.id)
767
768 title = T("Check-In")
769
770 get_vars = r.get_vars
771
772
773 location_id = get_vars.get("location_id", None)
774 if not location_id:
775
776 lat = get_vars.get("lat", None)
777 if lat is not None:
778 lon = get_vars.get("lon", None)
779 if lon is not None:
780 form_vars = Storage(lat = float(lat),
781 lon = float(lon),
782 )
783 form = Storage(vars=form_vars)
784 s3db.gis_location_onvalidation(form)
785 location_id = s3db.gis_location.insert(**form_vars)
786
787
788 form = None
789 if not location_id:
790
791
792
793 formstyle = current.deployment_settings.get_ui_formstyle()
794 row = formstyle("test", "test", "test", "test")
795 if isinstance(row, tuple):
796
797 tuple_rows = True
798 else:
799
800 tuple_rows = False
801
802 form_rows = []
803 comment = ""
804
805 _id = "location_id"
806 label = LABEL("%s:" % T("Location"))
807
808 from s3.s3widgets import S3LocationSelector
809 field = table.location_id
810
811
812 value = None
813 widget = S3LocationSelector(show_latlon = True)(field, value)
814
815 row = formstyle("%s__row" % _id, label, widget, comment)
816 if tuple_rows:
817 form_rows.append(row[0])
818 form_rows.append(row[1])
819 else:
820 form_rows.append(row)
821
822 _id = "submit"
823 label = ""
824 widget = INPUT(_type="submit", _value=T("Check-In"))
825 row = formstyle("%s__row" % _id, label, widget, comment)
826 if tuple_rows:
827 form_rows.append(row[0])
828 form_rows.append(row[1])
829 else:
830 form_rows.append(row)
831
832 if tuple_rows:
833
834 form = FORM(TABLE(*form_rows))
835 else:
836 form = FORM(*form_rows)
837
838 if form.accepts(current.request.vars, current.session):
839 location_id = form.vars.get("location_id", None)
840
841 if location_id:
842
843
844
845
846
847
848
849 tracker.set_location(location_id)
850 response.confirmation = T("Checked-In successfully!")
851
852 response.view = "check-in.html"
853 output = dict(form = form,
854 title = title,
855 )
856 return output
857
858
859 else:
860 raise HTTP(415, current.ERROR.BAD_FORMAT)
861
864 """
865 Custom Method to allow a trackable resource to check-out
866 """
867
868
869 @staticmethod
871 """
872 Apply method.
873
874 @param r: the S3Request
875 @param attr: controller options for this request
876 """
877
878 if r.representation == "html":
879
880 T = current.T
881
882 response = current.response
883 tracker = S3Trackable(r.table, record_id=r.id)
884
885 title = T("Check-Out")
886
887
888
889
890 formstyle = current.deployment_settings.get_ui_formstyle()
891 row = formstyle("test", "test", "test", "test")
892 if isinstance(row, tuple):
893
894 tuple_rows = True
895 else:
896
897 tuple_rows = False
898
899 form_rows = []
900 comment = ""
901
902 _id = "submit"
903 label = ""
904 widget = INPUT(_type="submit", _value=T("Check-Out"))
905 row = formstyle("%s__row" % _id, label, widget, comment)
906 if tuple_rows:
907 form_rows.append(row[0])
908 form_rows.append(row[1])
909 else:
910 form_rows.append(row)
911
912 if tuple_rows:
913
914 form = FORM(TABLE(*form_rows))
915 else:
916 form = FORM(*form_rows)
917
918 if form.accepts(current.request.vars, current.session):
919
920
921
922
923
924
925
926
927
928 tracker.set_location(r.record.location_id)
929 response.confirmation = T("Checked-Out successfully!")
930
931 response.view = "check-in.html"
932 output = dict(form = form,
933 title = title,
934 )
935 return output
936
937
938 else:
939 raise HTTP(415, current.ERROR.BAD_FORMAT)
940
941
942