Package vita :: Package modules :: Package eden :: Module pr
[frames] | no frames]

Source Code for Module vita.modules.eden.pr

   1  # -*- coding: utf-8 -*- 
   2   
   3  """ Sahana Eden Person Registry Model 
   4   
   5      @copyright: 2009-2012 (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  __all__ = ["S3PersonEntity", 
  31             "S3OrgAuthModel", 
  32             "S3PersonModel", 
  33             "S3GroupModel", 
  34             "S3ContactModel", 
  35             "S3PersonAddressModel", 
  36             "S3PersonImageModel", 
  37             "S3PersonIdentityModel", 
  38             "S3SavedSearch", 
  39             "S3PersonPresence", 
  40             "S3PersonDescription", 
  41             # Representation Methods 
  42             "pr_pentity_represent", 
  43             "pr_person_represent", 
  44             "pr_person_comment", 
  45             "pr_rheader", 
  46             # Custom Resource Methods 
  47             "pr_contacts", 
  48             "pr_profile", 
  49             # Hierarchy Manipulation 
  50             "pr_update_affiliations", 
  51             "pr_add_affiliation", 
  52             "pr_remove_affiliation", 
  53             # PE Helpers 
  54             "pr_get_pe_id", 
  55             # Back-end Role Tools 
  56             "pr_define_role", 
  57             "pr_delete_role", 
  58             "pr_add_to_role", 
  59             "pr_remove_from_role", 
  60             # Hierarchy Lookup 
  61             "pr_realm", 
  62             "pr_get_role_paths", 
  63             "pr_get_role_branches", 
  64             "pr_get_path", 
  65             "pr_get_ancestors", 
  66             "pr_get_descendants", 
  67             "pr_ancestors", 
  68             "pr_descendants", 
  69             # Internal Path Tools 
  70             "pr_rebuild_path", 
  71             "pr_role_rebuild_path"] 
  72   
  73  import gluon.contrib.simplejson as json 
  74   
  75  from gluon import * 
  76  from gluon.dal import Row 
  77  from gluon.storage import Storage 
  78  from gluon.sqlhtml import RadioWidget 
  79  #from ..s3 import * 
  80  from layouts import * 
  81  from eden.layouts import S3AddResourceLink 
  82   
  83  OU = 1 # role type which indicates hierarchy, see role_types 
  84  OTHER_ROLE = 9 
85 86 # ============================================================================= 87 -class S3PersonEntity(S3Model):
88 """ Person Super-Entity """ 89 90 names = ["pr_pentity", 91 "pr_affiliation", 92 "pr_role", 93 "pr_role_types", 94 "pr_role_id", 95 "pr_pe_label", 96 "pr_pe_types"] 97
98 - def model(self):
99 100 db = current.db 101 T = current.T 102 s3 = current.response.s3 103 104 add_component = self.add_component 105 configure = self.configure 106 crud_strings = s3.crud_strings 107 define_table = self.define_table 108 meta_fields= s3.meta_fields 109 super_entity = self.super_entity 110 super_key = self.super_key 111 super_link = self.super_link 112 113 YES = T("yes") #current.messages.YES 114 NO = T("no") #current.messages.NO 115 UNKNOWN_OPT = current.messages.UNKNOWN_OPT 116 117 # --------------------------------------------------------------------- 118 # Person Super-Entity 119 # 120 #if current.deployment_settings.get_ui_camp(): 121 # shelter = T("Camp") 122 #else: 123 # shelter = T("Shelter") 124 pe_types = Storage(pr_person = T("Person"), 125 pr_group = T("Group"), 126 org_organisation = T("Organization"), 127 org_office = T("Office"), 128 # If we want these, then pe_id needs adding to their 129 # tables & configuring as a super-entity 130 #cr_shelter = shelter, 131 #fire_station = T("Fire Station"), 132 #hms_hospital = T("Hospital"), 133 dvi_body = T("Body")) 134 135 tablename = "pr_pentity" 136 table = super_entity(tablename, "pe_id", pe_types, 137 Field("type"), 138 Field("pe_label", length=128)) 139 140 # Search method 141 pentity_search = S3PentitySearch(name = "pentity_search_simple", 142 label = T("Name and/or ID"), 143 comment = T(""), 144 field = ["pe_label"]) 145 146 pentity_search.pentity_represent = pr_pentity_represent 147 148 # Resource configuration 149 configure(tablename, 150 editable=False, 151 deletable=False, 152 listadd=False, 153 onaccept=self.pr_pentity_onaccept, 154 search_method=pentity_search) 155 156 # Reusable fields 157 pr_pe_label = S3ReusableField("pe_label", length=128, 158 label = T("ID Tag Number"), 159 requires = IS_NULL_OR(IS_NOT_ONE_OF(db, 160 "pr_pentity.pe_label"))) 161 162 # Components 163 pe_id = super_key(table) 164 add_component("pr_contact_emergency", pr_pentity=pe_id) 165 add_component("pr_address", pr_pentity=pe_id) 166 add_component("pr_image", pr_pentity=pe_id) 167 add_component("pr_contact", pr_pentity=pe_id) 168 add_component("pr_note", pr_pentity=pe_id) 169 add_component("pr_physical_description", 170 pr_pentity=dict(joinby=pe_id, 171 multiple=False)) 172 add_component("dvi_identification", 173 pr_pentity=dict(joinby=pe_id, 174 multiple=False)) 175 add_component("dvi_effects", 176 pr_pentity=dict(joinby=pe_id, 177 multiple=False)) 178 add_component("dvi_checklist", 179 pr_pentity=dict(joinby=pe_id, 180 multiple=False)) 181 # Map Configs 182 # - Personalised configurations 183 # - OU configurations (Organisation/Branch/Facility/Team) 184 add_component("gis_config", 185 pr_pentity=dict(joinby=pe_id, 186 multiple=False)) 187 188 # --------------------------------------------------------------------- 189 # Person <-> User 190 # 191 utable = current.auth.settings.table_user 192 tablename = "pr_person_user" 193 table = define_table(tablename, 194 super_link("pe_id", "pr_pentity"), 195 Field("user_id", utable), 196 *meta_fields()) 197 198 # --------------------------------------------------------------------- 199 # Role (Affiliates Group) 200 # 201 role_types = { 202 1:T("Organizational Units"), # business hierarchy (reporting units) 203 2:T("Membership"), # membership role 204 3:T("Association"), # other non-reporting role 205 9:T("Other") # other role type 206 } 207 tablename = "pr_role" 208 table = define_table(tablename, 209 # The "parent" entity 210 super_link("pe_id", "pr_pentity", 211 label=T("Corporate Entity"), 212 readable=True, 213 writable=True), 214 # Role type 215 Field("role_type", "integer", 216 requires = IS_IN_SET(role_types, zero=None), 217 represent = lambda opt: \ 218 role_types.get(opt, UNKNOWN_OPT)), 219 # Role name 220 Field("role", notnull=True), 221 # Path, for faster lookups 222 Field("path", 223 readable = False, 224 writable = False), 225 # Type filter, type of entities which can have this role 226 Field("entity_type", "string", 227 requires = IS_EMPTY_OR(IS_IN_SET(pe_types, zero=T("ANY"))), 228 represent = lambda opt: pe_types.get(opt, UNKNOWN_OPT)), 229 # Subtype filter, if the entity type defines its own type 230 Field("sub_type", "integer", 231 readable = False, 232 writable = False), 233 *meta_fields()) 234 235 # Field configuration 236 table.pe_id.requires = IS_ONE_OF(db, "pr_pentity.pe_id", 237 pr_pentity_represent, sort=True) 238 table.pe_id.represent = pr_pentity_represent 239 240 # CRUD Strings 241 crud_strings[tablename] = Storage( 242 title_create = T("Add Role"), 243 title_display = T("Role Details"), 244 title_list = T("Roles"), 245 title_update = T("Edit Role"), 246 title_search = T("Search Roles"), 247 subtitle_create = T("Add New Role"), 248 subtitle_list = T("List of Roles"), 249 label_list_button = T("List Roles"), 250 label_create_button = T("Add Role"), 251 label_delete_button = T("Delete Role"), 252 msg_record_created = T("Role added"), 253 msg_record_modified = T("Role updated"), 254 msg_record_deleted = T("Role deleted"), 255 msg_list_empty = T("No Roles defined")) 256 257 # Resource configuration 258 configure(tablename, 259 onvalidation=self.pr_role_onvalidation) 260 261 # Reusable fields 262 role_id = S3ReusableField("role_id", db.pr_role, 263 requires = IS_ONE_OF(db, "pr_role.id", 264 self.pr_role_represent), 265 represent = self.pr_role_represent, 266 label = T("Role"), 267 ondelete = "CASCADE") 268 269 # --------------------------------------------------------------------- 270 # Affiliation 271 # 272 tablename = "pr_affiliation" 273 table = define_table(tablename, 274 role_id(), 275 super_link("pe_id", "pr_pentity", 276 label=T("Entity"), 277 readable=True, 278 writable=True), 279 *meta_fields()) 280 281 table.pe_id.requires = IS_ONE_OF(db, "pr_pentity.pe_id", 282 pr_pentity_represent, sort=True) 283 table.pe_id.represent = pr_pentity_represent 284 285 # CRUD Strings 286 crud_strings[tablename] = Storage( 287 title_create = T("Add Affiliation"), 288 title_display = T("Affiliation Details"), 289 title_list = T("Affiliations"), 290 title_update = T("Edit Affiliation"), 291 title_search = T("Search Affiliations"), 292 subtitle_create = T("Add New Affiliation"), 293 subtitle_list = T("List of Affiliations"), 294 label_list_button = T("List Affiliations"), 295 label_create_button = T("Add Affiliation"), 296 label_delete_button = T("Delete Affiliation"), 297 msg_record_created = T("Affiliation added"), 298 msg_record_modified = T("Affiliation updated"), 299 msg_record_deleted = T("Affiliation deleted"), 300 msg_list_empty = T("No Affiliations defined")) 301 302 # Resource configuration 303 configure(tablename, 304 onaccept=self.pr_affiliation_onaccept, 305 ondelete=self.pr_affiliation_ondelete) 306 307 # --------------------------------------------------------------------- 308 # Return model-global names to response.s3 309 # 310 return Storage( 311 pr_pe_types=pe_types, 312 pr_pe_label=pr_pe_label, 313 pr_role_types=role_types, 314 pr_role_id=role_id, 315 )
316 317 # ------------------------------------------------------------------------- 318 @staticmethod
319 - def pr_role_represent(role_id):
320 """ 321 Represent an entity role 322 323 @param role_id: the pr_role record ID 324 """ 325 326 db = current.db 327 s3db = current.s3db 328 329 table = s3db.pr_role 330 role = db(table.id == role_id).select(table.role, 331 table.pe_id, 332 limitby=(0, 1)).first() 333 if role: 334 entity = pr_pentity_represent(role.pe_id) 335 return "%s: %s" % (entity, role.role) 336 else: 337 return current.messages.NONE
338 339 # ------------------------------------------------------------------------- 340 @staticmethod
341 - def pr_role_onvalidation(form):
342 """ 343 Clear descendant paths if role type has changed 344 345 @param form: the CRUD form 346 """ 347 348 db = current.db 349 s3db = current.s3db 350 351 formvars = form.vars 352 if not formvars: 353 return 354 if "role_type" in formvars: 355 role_id = form.record_id 356 if not role_id: 357 return 358 role_type = formvars.role_type 359 rtable = s3db.pr_role 360 role = db(rtable.id == role_id).select(rtable.role_type, 361 limitby=(0, 1)).first() 362 if role and str(role.role_type) != str(role_type): 363 # If role type has changed, then clear paths 364 if str(role_type) != str(OU): 365 formvars["path"] = None 366 s3db.pr_role_rebuild_path(role_id, clear=True) 367 return
368 369 # ------------------------------------------------------------------------- 370 @staticmethod
371 - def pr_pentity_onaccept(form):
372 """ 373 Update organisation affiliations for org_site instances. 374 """ 375 376 db = current.db 377 s3db = current.s3db 378 379 ptable = s3db.pr_pentity 380 381 pe_id = form.vars.pe_id 382 pe = db(ptable.pe_id == pe_id).select(ptable.instance_type, 383 limitby=(0, 1)).first() 384 if pe: 385 itable = s3db.table(pe.instance_type, None) 386 if itable and \ 387 "site_id" in itable.fields and \ 388 "organisation_id" in itable.fields: 389 q = itable.pe_id == pe_id 390 instance = db(q).select(itable.pe_id, 391 itable.organisation_id, 392 limitby=(0, 1)).first() 393 if instance: 394 s3db.pr_update_affiliations("org_site", instance) 395 return
396 397 # ------------------------------------------------------------------------- 398 @staticmethod
399 - def pr_affiliation_onaccept(form):
400 """ 401 Remove duplicate affiliations and clear descendant paths (to 402 trigger lazy rebuild) 403 404 @param form: the CRUD form 405 """ 406 407 db = current.db 408 s3db = current.s3db 409 manager = current.manager 410 411 atable = s3db.pr_affiliation 412 413 formvars = form.vars 414 role_id = formvars["role_id"] 415 pe_id = formvars["pe_id"] 416 record_id = formvars["id"] 417 418 if role_id and pe_id and record_id: 419 # Remove duplicates 420 query = (atable.id != record_id) & \ 421 (atable.role_id == role_id) & \ 422 (atable.pe_id == pe_id) 423 deleted_fk = {"role_id": role_id, "pe_id": pe_id} 424 data = {"deleted": True, 425 "role_id": None, 426 "pe_id": None, 427 "deleted_fk": json.dumps(deleted_fk)} 428 db(query).update(**data) 429 # Clear descendant paths 430 s3db.pr_rebuild_path(pe_id, clear=True) 431 return
432 433 # ------------------------------------------------------------------------- 434 @staticmethod
435 - def pr_affiliation_ondelete(row):
436 """ 437 Clear descendant paths, also called indirectly via 438 ondelete-CASCADE when a role gets deleted. 439 440 @param row: the deleted row 441 """ 442 443 db = current.db 444 s3db = current.s3db 445 446 atable = s3db.pr_affiliation 447 448 if row and row.id: 449 query = atable.id == row.id 450 record = db(query).select(atable.deleted_fk, 451 limitby=(0, 1)).first() 452 else: 453 return 454 if record: 455 data = json.loads(record.deleted_fk) 456 pe_id = data.get("pe_id", None) 457 if pe_id: 458 s3db.pr_rebuild_path(pe_id, clear=True) 459 return
460
461 # ============================================================================= 462 -class S3OrgAuthModel(S3Model):
463 """ Organisation-based Authorization Model """ 464 465 names = ["pr_delegation"] 466
467 - def model(self):
468 469 auth = current.auth 470 s3 = current.response.s3 471 role_id = current.s3db.pr_role_id 472 473 define_table = self.define_table 474 super_link = self.super_link 475 meta_fields = s3.meta_fields 476 477 # --------------------------------------------------------------------- 478 # Delegation: Role <-> Auth Group Link 479 # This "delegates" the permissions of a user group for the records 480 # owned by a person entity to a group of affiliated entities. 481 # 482 gtable = auth.settings.table_group 483 tablename = "pr_delegation" 484 table = define_table(tablename, 485 role_id(), 486 Field("group_id", gtable, 487 ondelete="CASCADE"), 488 *meta_fields()) 489 490 # --------------------------------------------------------------------- 491 return Storage()
492
493 # ============================================================================= 494 -class S3PersonModel(S3Model):
495 """ Persons and Groups """ 496 497 names = ["pr_person", 498 "pr_person_user", 499 "pr_gender", 500 "pr_gender_opts", 501 "pr_age_group", 502 "pr_age_group_opts", 503 "pr_person_id", 504 ] 505
506 - def model(self):
507 508 T = current.T 509 db = current.db 510 request = current.request 511 s3 = current.response.s3 512 gis = current.gis 513 settings = current.deployment_settings 514 515 pe_label = self.pr_pe_label 516 517 location_id = self.gis_location_id 518 519 messages = current.messages 520 UNKNOWN_OPT = messages.UNKNOWN_OPT 521 SELECT_LOCATION = messages.SELECT_LOCATION 522 523 define_table = self.define_table 524 super_link = self.super_link 525 add_component = self.add_component 526 527 # --------------------------------------------------------------------- 528 # Person 529 # 530 pr_gender_opts = { 531 1:"", 532 2:T("female"), 533 3:T("male") 534 } 535 pr_gender = S3ReusableField("gender", "integer", 536 requires = IS_IN_SET(pr_gender_opts, zero=None), 537 default = 1, 538 label = T("Gender"), 539 represent = lambda opt: \ 540 pr_gender_opts.get(opt, UNKNOWN_OPT)) 541 542 pr_age_group_opts = { 543 1:T("unknown"), 544 2:T("Infant (0-1)"), 545 3:T("Child (2-11)"), 546 4:T("Adolescent (12-20)"), 547 5:T("Adult (21-50)"), 548 6:T("Senior (50+)") 549 } 550 pr_age_group = S3ReusableField("age_group", "integer", 551 requires = IS_IN_SET(pr_age_group_opts, 552 zero=None), 553 default = 1, 554 label = T("Age Group"), 555 represent = lambda opt: \ 556 pr_age_group_opts.get(opt, 557 UNKNOWN_OPT)) 558 559 pr_marital_status_opts = { 560 1:T("unknown"), 561 2:T("single"), 562 3:T("married"), 563 4:T("separated"), 564 5:T("divorced"), 565 6:T("widowed"), 566 9:T("other") 567 } 568 569 pr_religion_opts = settings.get_L10n_religions() 570 571 pr_impact_tags = { 572 1: T("injured"), 573 4: T("diseased"), 574 2: T("displaced"), 575 5: T("separated from family"), 576 3: T("suffered financial losses") 577 } 578 579 if settings.get_L10n_mandatory_lastname(): 580 last_name_validate = IS_NOT_EMPTY(error_message = T("Please enter a last name")) 581 else: 582 last_name_validate = None 583 584 s3_date_format = settings.get_L10n_date_format() 585 586 tablename = "pr_person" 587 table = define_table(tablename, 588 super_link("pe_id", "pr_pentity"), 589 super_link("track_id", "sit_trackable"), 590 location_id(readable=False, 591 writable=False), # base location 592 pe_label(comment = DIV(DIV(_class="tooltip", 593 _title="%s|%s" % (T("ID Tag Number"), 594 T("Number or Label on the identification tag this person is wearing (if any)."))))), 595 Field("missing", "boolean", 596 readable=False, 597 writable=False, 598 default=False, 599 represent = lambda missing: \ 600 (missing and ["missing"] or [""])[0]), 601 Field("volunteer", "boolean", 602 readable=False, 603 writable=False, 604 default=False), 605 Field("first_name", 606 notnull=True, 607 default = "?" if current.auth.permission.format != "html" else "", 608 length=64, # Mayon Compatibility 609 # NB Not possible to have an IS_NAME() validator here 610 # http://eden.sahanafoundation.org/ticket/834 611 requires = IS_NOT_EMPTY(error_message = T("Please enter a first name")), 612 comment = DIV(_class="tooltip", 613 _title="%s|%s" % (T("First name"), 614 T("The first or only name of the person (mandatory)."))), 615 label = T("First Name")), 616 Field("middle_name", 617 length=64, # Mayon Compatibility 618 label = T("Middle Name")), 619 Field("last_name", 620 length=64, # Mayon Compatibility 621 label = T("Last Name"), 622 requires = last_name_validate), 623 Field("initials", 624 length=8, 625 label = T("Initials")), 626 Field("preferred_name", 627 label = T("Preferred Name"), 628 comment = DIV(DIV(_class="tooltip", 629 _title="%s|%s" % (T("Preferred Name"), 630 T("The name to be used when calling for or directly addressing the person (optional).")))), 631 length=64), # Mayon Compatibility 632 Field("local_name", 633 label = T("Local Name"), 634 comment = DIV(DIV(_class="tooltip", 635 _title="%s|%s" % (T("Local Name"), 636 T("Name of the person in local language and script (optional)."))))), 637 pr_gender(label = T("Gender")), 638 Field("date_of_birth", "date", 639 label = T("Date of Birth"), 640 requires = [IS_EMPTY_OR(IS_DATE_IN_RANGE( 641 format = s3_date_format, 642 maximum=request.utcnow.date(), 643 error_message="%s %%(max)s!" % 644 T("Enter a valid date before")))], 645 widget = S3DateWidget(past=1320, # Months, so 110 years 646 future=0)), 647 pr_age_group(label = T("Age group")), 648 Field("nationality", 649 requires = IS_NULL_OR(IS_IN_SET_LAZY( 650 lambda: gis.get_countries(key_type="code"), 651 zero = SELECT_LOCATION)), 652 label = T("Nationality"), 653 comment = DIV(DIV(_class="tooltip", 654 _title="%s|%s" % (T("Nationality"), 655 T("Nationality of the person.")))), 656 represent = lambda code: \ 657 gis.get_country(code, key_type="code") or UNKNOWN_OPT), 658 Field("religion", length=128, 659 label = T("Religion"), 660 requires = IS_NULL_OR(IS_IN_SET(pr_religion_opts)), 661 represent = lambda opt: \ 662 pr_religion_opts.get(opt, UNKNOWN_OPT), 663 ), 664 Field("occupation", length=128, # Mayon Compatibility 665 label = T("Profession"), 666 ), 667 # Field("picture", "upload", 668 # autodelete=True, 669 # label = T("Picture"), 670 # requires = IS_EMPTY_OR(IS_IMAGE(maxsize=(800, 800), 671 # error_message=T("Upload an image file (bmp, gif, jpeg or png), max. 800x800 pixels!"))), 672 # represent = lambda image: image and \ 673 # DIV(A(IMG(_src=URL(c="default", f="download", 674 # args=image), 675 # _height=60, 676 # _alt=T("View Picture")), 677 # _href=URL(c="default", f="download", 678 # args=image))) or 679 # T("No Picture"), 680 # comment = DIV(_class="tooltip", 681 # _title="%s|%s" % (T("Picture"), 682 # T("Upload an image file here.")))), 683 Field("opt_in", 684 "string", # list of mailing lists which link to teams 685 default=False, 686 label = T("Receive updates"), 687 comment = DIV(DIV(_class="tooltip", 688 _title="%s|%s" % (T("Mailing list"), 689 T("By selecting this you agree that we may contact you.")))), 690 ), 691 s3.comments(), 692 *(s3.lx_fields() + s3.meta_fields())) 693 694 # CRUD Strings 695 ADD_PERSON = current.messages.ADD_PERSON 696 LIST_PERSONS = T("List Persons") 697 s3.crud_strings[tablename] = Storage( 698 title_create = T("Add a Person"), 699 title_display = T("Person Details"), 700 title_list = LIST_PERSONS, 701 title_update = T("Edit Person Details"), 702 title_search = T("Search Persons"), 703 subtitle_create = ADD_PERSON, 704 subtitle_list = T("Persons"), 705 label_list_button = LIST_PERSONS, 706 label_create_button = ADD_PERSON, 707 label_delete_button = T("Delete Person"), 708 msg_record_created = T("Person added"), 709 msg_record_modified = T("Person details updated"), 710 msg_record_deleted = T("Person deleted"), 711 msg_list_empty = T("No Persons currently registered")) 712 713 # add an opt in clause to receive emails depending on the deployment settings 714 if current.deployment_settings.get_auth_opt_in_to_email(): 715 table.opt_in.readable = True 716 table.opt_in.writable = True 717 else: 718 table.opt_in.readable = False 719 table.opt_in.writable = False 720 721 # Search method 722 pr_person_search = S3PersonSearch( 723 name="person_search_simple", 724 label=T("Name and/or ID"), 725 comment=T("To search for a person, enter any of the first, middle or last names and/or an ID number of a person, separated by spaces. You may use % as wildcard. Press 'Search' without input to list all persons."), 726 field=["pe_label", 727 "first_name", 728 "middle_name", 729 "last_name", 730 "local_name", 731 "identity.value" 732 ]) 733 734 # Resource configuration 735 self.configure(tablename, 736 super_entity=("pr_pentity", "sit_trackable"), 737 list_fields = ["id", 738 "first_name", 739 "middle_name", 740 "last_name", 741 #"picture", 742 "gender", 743 "age_group", 744 (T("Organization"), "hrm_human_resource:organisation_id$name") 745 ], 746 onvalidation=self.pr_person_onvalidation, 747 search_method=pr_person_search, 748 deduplicate=self.person_deduplicate, 749 main="first_name", 750 extra="last_name") 751 752 person_id_comment = pr_person_comment( 753 T("Person"), 754 T("Type the first few characters of one of the Person's names."), 755 child="person_id") 756 757 person_id = S3ReusableField("person_id", db.pr_person, 758 sortby = ["first_name", "middle_name", "last_name"], 759 requires = IS_NULL_OR(IS_ONE_OF(db, "pr_person.id", 760 pr_person_represent, 761 orderby="pr_person.first_name", 762 sort=True, 763 error_message=T("Person must be specified!"))), 764 represent = pr_person_represent, 765 label = T("Person"), 766 comment = person_id_comment, 767 ondelete = "RESTRICT", 768 widget = S3PersonAutocompleteWidget()) 769 770 # Components 771 add_component("pr_group_membership", pr_person="person_id") 772 add_component("pr_identity", pr_person="person_id") 773 add_component("pr_save_search", pr_person="person_id") 774 add_component("msg_subscription", pr_person="person_id") 775 776 # HR Record as component of Persons 777 add_component("hrm_human_resource", pr_person="person_id") 778 add_component("member_membership", pr_person="person_id") 779 780 # Skills as components of Persons 781 add_component("hrm_certification", pr_person="person_id") 782 add_component("hrm_competency", pr_person="person_id") 783 add_component("hrm_credential", pr_person="person_id") 784 add_component("hrm_experience", pr_person="person_id") 785 # @ToDo: Double link table to show the Courses attended? 786 add_component("hrm_training", pr_person="person_id") 787 788 # Assets as component of persons 789 add_component("asset_asset", pr_person="assigned_to_id") 790 791 # --------------------------------------------------------------------- 792 # Return model-global names to response.s3 793 # 794 return Storage( 795 pr_gender = pr_gender, 796 pr_gender_opts = pr_gender_opts, 797 pr_age_group = pr_age_group, 798 pr_age_group_opts = pr_age_group_opts, 799 pr_person_id = person_id, 800 )
801 802 # ------------------------------------------------------------------------- 803 @staticmethod
804 - def pr_person_onvalidation(form):
805 """ Onvalidation callback """ 806 807 try: 808 age = int(form.vars.get("age_group", None)) 809 except (ValueError, TypeError): 810 age = None 811 dob = form.vars.get("date_of_birth", None) 812 813 if age and age != 1 and dob: 814 now = request.utcnow 815 dy = int((now.date() - dob).days / 365.25) 816 if dy < 0: 817 ag = 1 818 elif dy < 2: 819 ag = 2 820 elif dy < 12: 821 ag = 3 822 elif dy < 21: 823 ag = 4 824 elif dy < 51: 825 ag = 5 826 else: 827 ag = 6 828 829 if age != ag: 830 form.errors.age_group = T("Age group does not match actual age.") 831 return False 832 833 return True
834 835 # ------------------------------------------------------------------------- 836 @staticmethod
837 - def person_deduplicate(item):
838 """ Import item deduplication """ 839 840 db = current.db 841 s3db = current.s3db 842 843 # Ignore this processing if the id is set 844 if item.id: 845 return 846 if item.tablename == "pr_person": 847 ptable = s3db.pr_person 848 ctable = s3db.pr_contact 849 850 # Match by first name and last name, and if given, by email address 851 fname = "first_name" in item.data and item.data.first_name 852 lname = "last_name" in item.data and item.data.last_name 853 if fname and lname: 854 # "LIKE" is inappropriate here: 855 # E.g. "Fran Boon" would overwrite "Frank Boones" 856 #query = (ptable.first_name.lower().like('%%%s%%' % fname.lower())) & \ 857 #(ptable.last_name.lower().like('%%%s%%' % lname.lower())) 858 859 # But even an exact name match does not necessarily indicate a 860 # duplicate: depending on the scope of the deployment, you could 861 # have thousands of people with exactly the same names (or just 862 # two of them - and it can already go wrong). 863 # We take the email address as additional criterion, however, where 864 # person data do not usually contain email addresses you might need 865 # to add more/other criteria here 866 query = (ptable.first_name.lower() == fname.lower()) & \ 867 (ptable.last_name.lower() == lname.lower()) 868 email = False 869 for citem in item.components: 870 if citem.tablename == "pr_contact": 871 if "contact_method" in citem.data and \ 872 citem.data.contact_method == "EMAIL": 873 email = citem.data.value 874 if email != False: 875 query = query & \ 876 (ptable.pe_id == ctable.pe_id) & \ 877 (ctable.value.lower() == email.lower()) 878 879 else: 880 # Try Initials (this is a weak test but works well in small teams) 881 initials = "initials" in item.data and item.data.initials 882 if not initials: 883 # Nothing we can do 884 return 885 query = (ptable.initials.lower() == initials.lower()) 886 887 # Look for details on the database 888 _duplicate = db(query).select(ptable.id, 889 limitby=(0, 1)).first() 890 if _duplicate: 891 item.id = _duplicate.id 892 item.data.id = _duplicate.id 893 item.method = item.METHOD.UPDATE 894 for citem in item.components: 895 citem.method = citem.METHOD.UPDATE
896
897 # ============================================================================= 898 -class S3GroupModel(S3Model):
899 """ Groups """ 900 901 names = ["pr_group", 902 "pr_group_id", 903 "pr_group_represent", 904 "pr_group_membership"] 905
906 - def model(self):
907 908 T = current.T 909 db = current.db 910 request = current.request 911 s3 = current.response.s3 912 913 person_id = self.pr_person_id 914 915 messages = current.messages 916 NONE = messages.NONE 917 UNKNOWN_OPT = messages.UNKNOWN_OPT 918 919 comments = s3.comments 920 configure = self.configure 921 crud_strings = s3.crud_strings 922 define_table = self.define_table 923 meta_fields = s3.meta_fields 924 925 # --------------------------------------------------------------------- 926 # Group 927 # 928 pr_group_types = { 929 1:T("Family"), 930 2:T("Tourist Group"), 931 3:T("Relief Team"), 932 4:T("other"), 933 5:T("Mailing Lists"), 934 } 935 936 tablename = "pr_group" 937 table = define_table(tablename, 938 self.super_link("pe_id", "pr_pentity"), 939 Field("group_type", "integer", 940 requires = IS_IN_SET(pr_group_types, zero=None), 941 default = 4, 942 label = T("Group Type"), 943 represent = lambda opt: \ 944 pr_group_types.get(opt, UNKNOWN_OPT)), 945 Field("system", "boolean", 946 default=False, 947 readable=False, 948 writable=False), 949 Field("name", 950 label=T("Group Name"), 951 requires = IS_NOT_EMPTY()), 952 Field("description", 953 label=T("Group Description")), 954 comments(), 955 *meta_fields()) 956 957 # Field configuration 958 table.description.comment = DIV(DIV(_class="tooltip", 959 _title="%s|%s" % (T("Group description"), 960 T("A brief description of the group (optional)")))) 961 962 # CRUD Strings 963 ADD_GROUP = T("Add Group") 964 LIST_GROUPS = T("List Groups") 965 crud_strings[tablename] = Storage( 966 title_create = ADD_GROUP, 967 title_display = T("Group Details"), 968 title_list = LIST_GROUPS, 969 title_update = T("Edit Group"), 970 title_search = T("Search Groups"), 971 subtitle_create = T("Add New Group"), 972 subtitle_list = T("Groups"), 973 label_list_button = LIST_GROUPS, 974 label_create_button = ADD_GROUP, 975 label_delete_button = T("Delete Group"), 976 msg_record_created = T("Group added"), 977 msg_record_modified = T("Group updated"), 978 msg_record_deleted = T("Group deleted"), 979 msg_list_empty = T("No Groups currently registered")) 980 981 # CRUD Strings 982 ADD_GROUP = T("Add Mailing List") 983 LIST_GROUPS = T("Mailing List") 984 mailing_list_crud_strings = Storage( 985 title_create = ADD_GROUP, 986 title_display = T("Mailing List Details"), 987 title_list = LIST_GROUPS, 988 title_update = T("Edit Mailing List"), 989 title_search = T("Search Mailing Lists"), 990 subtitle_create = T("Add New Mailing List"), 991 subtitle_list = T("Mailing Lists"), 992 label_list_button = LIST_GROUPS, 993 label_create_button = ADD_GROUP, 994 label_delete_button = T("Delete Mailing List"), 995 msg_record_created = T("Mailing list added"), 996 msg_record_modified = T("Mailing list updated"), 997 msg_record_deleted = T("Mailing list deleted"), 998 msg_list_empty = T("No Mailing List currently established")) 999 1000 # Resource configuration 1001 configure(tablename, 1002 super_entity="pr_pentity", 1003 deduplicate=self.group_deduplicate, 1004 main="name", 1005 extra="description") 1006 1007 # Reusable fields 1008 group_represent = lambda id: (id and [db.pr_group[id].name] or [NONE])[0] 1009 group_id = S3ReusableField("group_id", db.pr_group, 1010 sortby="name", 1011 requires = IS_NULL_OR(IS_ONE_OF(db, "pr_group.id", 1012 "%(id)s: %(name)s", 1013 filterby="system", 1014 filter_opts=(False,))), 1015 represent = group_represent, 1016 comment=S3AddResourceLink(c="pr", 1017 f="group", 1018 label=s3.crud_strings.pr_group.label_create_button, 1019 title=T("Create Group Entry"), 1020 tooltip=T("Create a group entry in the registry.")), 1021 ondelete = "RESTRICT") 1022 1023 # Components 1024 self.add_component("pr_group_membership", pr_group="group_id") 1025 1026 # --------------------------------------------------------------------- 1027 # Group membership 1028 # 1029 resourcename = "group_membership" 1030 tablename = "pr_group_membership" 1031 table = define_table(tablename, 1032 group_id(label = T("Group"), 1033 ondelete="CASCADE"), 1034 person_id(label = T("Person"), 1035 ondelete="CASCADE"), 1036 Field("group_head", "boolean", 1037 label = T("Group Head"), 1038 default=False), 1039 Field("description", 1040 label = T("Description")), 1041 comments(), 1042 *meta_fields()) 1043 1044 # Field configuration 1045 table.group_head.represent = lambda group_head: \ 1046 (group_head and [T("yes")] or [""])[0] 1047 1048 # CRUD strings 1049 request = current.request 1050 if request.function in ("person", "group_membership"): 1051 crud_strings[tablename] = Storage( 1052 title_create = T("Add Membership"), 1053 title_display = T("Membership Details"), 1054 title_list = T("Memberships"), 1055 title_update = T("Edit Membership"), 1056 title_search = T("Search Membership"), 1057 subtitle_create = T("Add New Membership"), 1058 subtitle_list = T("Current Memberships"), 1059 label_list_button = T("List All Memberships"), 1060 label_create_button = T("Add Membership"), 1061 label_delete_button = T("Delete Membership"), 1062 msg_record_created = T("Membership added"), 1063 msg_record_modified = T("Membership updated"), 1064 msg_record_deleted = T("Membership deleted"), 1065 msg_list_empty = T("No Memberships currently registered")) 1066 1067 elif request.function == "group": 1068 crud_strings[tablename] = Storage( 1069 title_create = T("Add Member"), 1070 title_display = T("Membership Details"), 1071 title_list = T("Group Members"), 1072 title_update = T("Edit Membership"), 1073 title_search = T("Search Member"), 1074 subtitle_create = T("Add New Member"), 1075 subtitle_list = T("Current Group Members"), 1076 label_list_button = T("List Members"), 1077 label_create_button = T("Add Group Member"), 1078 label_delete_button = T("Delete Membership"), 1079 msg_record_created = T("Group Member added"), 1080 msg_record_modified = T("Membership updated"), 1081 msg_record_deleted = T("Membership deleted"), 1082 msg_list_empty = T("No Members currently registered")) 1083 1084 # Resource configuration 1085 configure(tablename, 1086 onaccept = self.group_membership_onaccept, 1087 ondelete = self.group_membership_onaccept, 1088 list_fields=["id", 1089 "group_id", 1090 "person_id", 1091 "group_head", 1092 "description" 1093 ]) 1094 1095 # --------------------------------------------------------------------- 1096 # Return model-global names to response.s3 1097 # 1098 return Storage( 1099 pr_group_id = group_id, 1100 pr_group_represent = group_represent, 1101 pr_mailing_list_crud_strings = mailing_list_crud_strings 1102 )
1103 1104 # ------------------------------------------------------------------------- 1105 @staticmethod
1106 - def group_deduplicate(item):
1107 """ Group de-duplication """ 1108 1109 if item.id: 1110 return 1111 if item.tablename == "pr_group": 1112 table = item.table 1113 name = item.data.get("name", None) 1114 1115 query = (table.name == name) & \ 1116 (table.deleted != True) 1117 duplicate = current.db(query).select(table.id, 1118 limitby=(0, 1)).first() 1119 if duplicate: 1120 item.id = duplicate.id 1121 item.method = item.METHOD.UPDATE 1122 return
1123 1124 # ------------------------------------------------------------------------- 1125 @staticmethod
1126 - def group_membership_onaccept(form):
1127 """ 1128 Remove any duplicate memberships and update affiliations 1129 """ 1130 1131 db = current.db 1132 s3db = current.s3db 1133 1134 mtable = s3db.pr_group_membership 1135 1136 if hasattr(form, "vars"): 1137 _id = form.vars.id 1138 elif isinstance(form, Row) and "id" in form: 1139 _id = form.id 1140 else: 1141 return 1142 if _id: 1143 record = db(mtable.id == _id).select(limitby=(0, 1)).first() 1144 else: 1145 return 1146 if record: 1147 person_id = record.person_id 1148 group_id = record.group_id 1149 if person_id and group_id and not record.deleted: 1150 query = (mtable.person_id == person_id) & \ 1151 (mtable.group_id == group_id) & \ 1152 (mtable.id != record.id) & \ 1153 (mtable.deleted != True) 1154 deleted_fk = {"person_id": person_id, 1155 "group_id": group_id} 1156 db(query).update(deleted = True, 1157 person_id = None, 1158 group_id = None, 1159 deleted_fk = json.dumps(deleted_fk)) 1160 pr_update_affiliations(mtable, record) 1161 return
1162
1163 # ============================================================================= 1164 -class S3ContactModel(S3Model):
1165 """ Person Contacts """ 1166 1167 names = ["pr_contact", 1168 "pr_contact_emergency" 1169 ] 1170
1171 - def model(self):
1172 1173 T = current.T 1174 db = current.db 1175 msg = current.msg 1176 request = current.request 1177 s3 = current.response.s3 1178 1179 UNKNOWN_OPT = current.messages.UNKNOWN_OPT 1180 1181 comments = s3.comments 1182 define_table = self.define_table 1183 meta_fields = s3.meta_fields 1184 super_link = self.super_link 1185 1186 # --------------------------------------------------------------------- 1187 # Contact 1188 # 1189 # @ToDo: Provide widgets which can be dropped into the main person form to have 1190 # the relevant ones for that deployment/context collected on that same 1191 # form 1192 # 1193 contact_methods = msg.CONTACT_OPTS 1194 1195 tablename = "pr_contact" 1196 table = define_table(tablename, 1197 super_link("pe_id", "pr_pentity"), 1198 Field("contact_method", 1199 length=32, 1200 requires = IS_IN_SET(contact_methods, 1201 zero=None), 1202 default = "SMS", 1203 label = T("Contact Method"), 1204 represent = lambda opt: \ 1205 contact_methods.get(opt, UNKNOWN_OPT)), 1206 Field("value", 1207 label= T("Value"), 1208 notnull=True, 1209 requires = IS_NOT_EMPTY()), 1210 Field("priority", "integer", 1211 label= T("Priority"), 1212 comment = DIV(_class="tooltip", 1213 _title="%s|%s" % (T("Priority"), 1214 T("What order to be contacted in."))), 1215 requires = IS_IN_SET(range(1, 10), zero=None)), 1216 comments(), 1217 *meta_fields()) 1218 1219 # Field configuration 1220 table.pe_id.requires = IS_ONE_OF(db, "pr_pentity.pe_id", 1221 pr_pentity_represent, 1222 orderby="instance_type", 1223 filterby="instance_type", 1224 filter_opts=("pr_person", "pr_group")) 1225 1226 # CRUD Strings 1227 s3.crud_strings[tablename] = Storage( 1228 title_create = T("Add Contact Information"), 1229 title_display = T("Contact Details"), 1230 title_list = T("Contact Information"), 1231 title_update = T("Edit Contact Information"), 1232 title_search = T("Search Contact Information"), 1233 subtitle_create = T("Add Contact Information"), 1234 subtitle_list = T("Contact Information"), 1235 label_list_button = T("List Contact Information"), 1236 label_create_button = T("Add Contact Information"), 1237 label_delete_button = T("Delete Contact Information"), 1238 msg_record_created = T("Contact Information Added"), 1239 msg_record_modified = T("Contact Information Updated"), 1240 msg_record_deleted = T("Contact Information Deleted"), 1241 msg_list_empty = T("No contact information available")) 1242 1243 # Resource configuration 1244 self.configure(tablename, 1245 onvalidation=self.contact_onvalidation, 1246 deduplicate=self.contact_deduplicate, 1247 list_fields=["id", 1248 "contact_method", 1249 "value", 1250 "priority", 1251 ]) 1252 1253 # --------------------------------------------------------------------- 1254 # Emergency Contact Information 1255 # 1256 tablename = "pr_contact_emergency" 1257 table = define_table(tablename, 1258 super_link("pe_id", "pr_pentity"), 1259 Field("name", 1260 label= T("Name")), 1261 Field("relationship", 1262 label= T("Relationship")), 1263 Field("phone", 1264 label = T("Phone"), 1265 requires = IS_NULL_OR(s3_phone_requires)), 1266 comments(), 1267 *meta_fields()) 1268 1269 # --------------------------------------------------------------------- 1270 # Return model-global names to response.s3 1271 # 1272 return Storage( 1273 )
1274 1275 # ------------------------------------------------------------------------- 1276 @staticmethod
1277 - def contact_onvalidation(form):
1278 """ Contact form validation """ 1279 1280 if form.vars.contact_method == "EMAIL": 1281 email, error = IS_EMAIL()(form.vars.value) 1282 if error: 1283 form.errors.value = T("Enter a valid email") 1284 return False
1285 1286 # ------------------------------------------------------------------------- 1287 @staticmethod
1288 - def contact_deduplicate(item):
1289 """ Contact information de-duplication """ 1290 1291 if item.id: 1292 return 1293 if item.tablename == "pr_contact": 1294 table = item.table 1295 pe_id = item.data.get("pe_id", None) 1296 contact_method = item.data.get("contact_method", None) 1297 value = item.data.get("value", None) 1298 1299 if pe_id is None: 1300 return 1301 1302 query = (table.pe_id == pe_id) & \ 1303 (table.contact_method == contact_method) & \ 1304 (table.value == value) & \ 1305 (table.deleted != True) 1306 duplicate = current.db(query).select(table.id, 1307 limitby=(0, 1)).first() 1308 if duplicate: 1309 item.id = duplicate.id 1310 item.method = item.METHOD.UPDATE 1311 return
1312
1313 # ============================================================================= 1314 -class S3PersonAddressModel(S3Model):
1315 """ Addresses for Persons """ 1316 1317 names = ["pr_address", 1318 "pr_address_type_opts" 1319 ] 1320
1321 - def model(self):
1322 1323 T = current.T 1324 db = current.db 1325 request = current.request 1326 s3 = current.response.s3 1327 1328 location_id = self.gis_location_id 1329 1330 UNKNOWN_OPT = current.messages.UNKNOWN_OPT 1331 1332 # --------------------------------------------------------------------- 1333 # Address 1334 # 1335 pr_address_type_opts = { 1336 1:T("Current Home Address"), 1337 2:T("Permanent Home Address"), 1338 3:T("Office Address"), 1339 #3:T("Holiday Address"), 1340 9:T("Other Address") 1341 } 1342 1343 tablename = "pr_address" 1344 table = self.define_table(tablename, 1345 self.super_link("pe_id", "pr_pentity"), 1346 Field("type", "integer", 1347 requires = IS_IN_SET(pr_address_type_opts, zero=None), 1348 widget = RadioWidget.widget, 1349 default = 1, 1350 label = T("Address Type"), 1351 represent = lambda opt: \ 1352 pr_address_type_opts.get(opt, UNKNOWN_OPT)), 1353 location_id(), 1354 s3.comments(), 1355 *(s3.address_fields() + s3.meta_fields())) 1356 1357 table.pe_id.requires = IS_ONE_OF(db, "pr_pentity.pe_id", 1358 pr_pentity_represent, 1359 orderby="instance_type", 1360 filterby="instance_type", 1361 filter_opts=("pr_person", "pr_group")) 1362 1363 # Field configuration 1364 if not self.settings.get_gis_building_name(): 1365 table.building_name.readable = False 1366 1367 # CRUD Strings 1368 ADD_ADDRESS = T("Add Address") 1369 LIST_ADDRESS = T("List of addresses") 1370 s3.crud_strings[tablename] = Storage( 1371 title_create = ADD_ADDRESS, 1372 title_display = T("Address Details"), 1373 title_list = LIST_ADDRESS, 1374 title_update = T("Edit Address"), 1375 title_search = T("Search Addresses"), 1376 subtitle_create = T("Add New Address"), 1377 subtitle_list = T("Addresses"), 1378 label_list_button = LIST_ADDRESS, 1379 label_create_button = ADD_ADDRESS, 1380 msg_record_created = T("Address added"), 1381 msg_record_modified = T("Address updated"), 1382 msg_record_deleted = T("Address deleted"), 1383 msg_list_empty = T("There is no address for this person yet. Add new address.")) 1384 1385 # Resource configuration 1386 self.configure(tablename, 1387 onaccept=self.address_onaccept, 1388 onvalidation=s3.address_onvalidation, 1389 deduplicate=self.address_deduplicate, 1390 list_fields = ["id", 1391 "type", 1392 "address", 1393 "postcode", 1394 #"L4", 1395 "L3", 1396 "L2", 1397 "L1", 1398 "L0" 1399 ]) 1400 1401 # --------------------------------------------------------------------- 1402 # Return model-global names to response.s3 1403 # 1404 return Storage( 1405 pr_address_type_opts = pr_address_type_opts 1406 )
1407 1408 # ------------------------------------------------------------------------- 1409 @staticmethod
1410 - def address_onaccept(form):
1411 """ 1412 Updates the Base Location to be the same as the Address 1413 1414 If the base location hasn't yet been set or if this is specifically 1415 requested 1416 """ 1417 1418 db = current.db 1419 s3db = current.s3db 1420 request = current.request 1421 settings = current.deployment_settings 1422 lx_update = current.response.s3.lx_update 1423 1424 vars = form.vars 1425 location_id = vars.location_id 1426 pe_id = vars.pe_id 1427 1428 if location_id: 1429 person = None 1430 table = s3db.pr_person 1431 if "base_location" in request.vars and \ 1432 request.vars.base_location == "on": 1433 # Specifically requested 1434 S3Tracker()(s3db.pr_pentity, pe_id).set_base_location(location_id) 1435 person = db(table.pe_id == pe_id).select(table.id, 1436 limitby=(0, 1)).first() 1437 if person: 1438 # Update the Lx fields 1439 lx_update(table, person.id) 1440 else: 1441 # Check if a base location already exists 1442 query = (table.pe_id == pe_id) 1443 person = db(query).select(table.id, 1444 table.location_id).first() 1445 if person and not person.location_id: 1446 # Hasn't yet been set so use this 1447 S3Tracker()(s3db.pr_pentity, pe_id).set_base_location(location_id) 1448 # Update the Lx fields 1449 lx_update(table, person.id) 1450 1451 if person and str(vars.type) == "1": # Home Address 1452 if settings.has_module("hrm"): 1453 # Also check for any Volunteer HRM record(s) 1454 htable = s3db.hrm_human_resource 1455 query = (htable.person_id == person.id) & \ 1456 (htable.type == 2) & \ 1457 (htable.deleted != True) 1458 hrs = db(query).select(htable.id) 1459 for hr in hrs: 1460 db(htable.id == hr.id).update(location_id=location_id) 1461 # Update the Lx fields 1462 lx_update(htable, hr.id) 1463 if settings.has_module("member"): 1464 # Also check for any Member record(s) 1465 mtable = s3db.member_membership 1466 query = (mtable.person_id == person.id) & \ 1467 (mtable.deleted != True) 1468 members = db(query).select(mtable.id) 1469 for member in members: 1470 db(mtable.id == member.id).update(location_id=location_id) 1471 # Update the Lx fields 1472 lx_update(mtable, member.id) 1473 return
1474 1475 # ------------------------------------------------------------------------- 1476 @staticmethod
1477 - def address_deduplicate(item):
1478 """ Address de-duplication """ 1479 1480 if item.id: 1481 return 1482 if item.tablename == "pr_address": 1483 table = item.table 1484 pe_id = item.data.get("pe_id", None) 1485 address = item.data.get("address", None) 1486 1487 if pe_id is None: 1488 return 1489 1490 query = (table.pe_id == pe_id) & \ 1491 (table.address == address) & \ 1492 (table.deleted != True) 1493 duplicate = current.db(query).select(table.id, 1494 limitby=(0, 1)).first() 1495 if duplicate: 1496 item.id = duplicate.id 1497 item.method = item.METHOD.UPDATE 1498 return
1499
1500 # ============================================================================= 1501 -class S3PersonImageModel(S3Model):
1502 """ Images for Persons """ 1503 1504 names = ["pr_image"] 1505
1506 - def model(self):
1507 1508 T = current.T 1509 db = current.db 1510 request = current.request 1511 s3 = current.response.s3 1512 1513 UNKNOWN_OPT = current.messages.UNKNOWN_OPT 1514 1515 # --------------------------------------------------------------------- 1516 # Image 1517 # 1518 pr_image_type_opts = { 1519 1:T("Photograph"), 1520 2:T("Sketch"), 1521 3:T("Fingerprint"), 1522 4:T("X-Ray"), 1523 5:T("Document Scan"), 1524 9:T("other") 1525 } 1526 1527 tablename = "pr_image" 1528 table = self.define_table(tablename, 1529 self.super_link("pe_id", "pr_pentity"), 1530 Field("profile", "boolean", 1531 default = False, 1532 label = T("Profile Picture?") 1533 ), 1534 Field("type", "integer", 1535 requires = IS_IN_SET(pr_image_type_opts, zero=None), 1536 default = 1, 1537 label = T("Image Type"), 1538 represent = lambda opt: pr_image_type_opts.get(opt, 1539 UNKNOWN_OPT)), 1540 Field("title", label=T("Title"), 1541 requires = IS_NOT_EMPTY(), 1542 comment = DIV(_class="tooltip", 1543 _title="%s|%s" % (T("Title"), 1544 T("Specify a descriptive title for the image.")))), 1545 Field("image", "upload", autodelete=True, 1546 represent = lambda image: image and \ 1547 DIV(A(IMG(_src=URL(c="default", f="download", 1548 args=image), 1549 _height=60, _alt=T("View Image")), 1550 _href=URL(c="default", f="download", 1551 args=image))) or T("No Image"), 1552 comment = DIV(_class="tooltip", 1553 _title="%s|%s" % (T("Image"), 1554 T("Upload an image file here. If you don't upload an image file, then you must specify its location in the URL field.")))), 1555 Field("url", 1556 label = T("URL"), 1557 represent = lambda url: url and \ 1558 DIV(A(IMG(_src=url, _height=60), _href=url)) or T("None"), 1559 comment = DIV(_class="tooltip", 1560 _title="%s|%s" % (T("URL"), 1561 T("The URL of the image file. If you don't upload an image file, then you must specify its location here.")))), 1562 Field("description", 1563 label=T("Description"), 1564 comment = DIV(_class="tooltip", 1565 _title="%s|%s" % (T("Description"), 1566 T("Give a brief description of the image, e.g. what can be seen where on the picture (optional).")))), 1567 s3.comments(), 1568 *s3.meta_fields()) 1569 1570 # CRUD Strings 1571 LIST_IMAGES = T("List Images") 1572 s3.crud_strings[tablename] = Storage( 1573 title_create = T("Image"), 1574 title_display = T("Image Details"), 1575 title_list = LIST_IMAGES, 1576 title_update = T("Edit Image Details"), 1577 title_search = T("Search Images"), 1578 subtitle_create = T("Add New Image"), 1579 subtitle_list = T("Images"), 1580 label_list_button = LIST_IMAGES, 1581 label_create_button = T("Add Image"), 1582 label_delete_button = T("Delete Image"), 1583 msg_record_created = T("Image added"), 1584 msg_record_modified = T("Image updated"), 1585 msg_record_deleted = T("Image deleted"), 1586 msg_list_empty = T("No Images currently registered")) 1587 1588 # Resource configuration 1589 self.configure(tablename, 1590 onaccept = self.pr_image_onaccept, 1591 onvalidation = self.pr_image_onvalidation, 1592 mark_required = ["url", "image"], 1593 list_fields=["id", 1594 "title", 1595 "profile", 1596 "type", 1597 "image", 1598 "url", 1599 "description" 1600 ]) 1601 1602 # --------------------------------------------------------------------- 1603 # Return model-global names to response.s3 1604 # 1605 return Storage()
1606 1607 # ------------------------------------------------------------------------- 1608 @staticmethod
1609 - def pr_image_onaccept(form):
1610 """ 1611 If this is the profile image then remove this flag from all 1612 others for this person. 1613 """ 1614 1615 db = current.db 1616 s3db = current.s3db 1617 table = s3db.pr_image 1618 1619 vars = form.vars 1620 id = vars.id 1621 profile = vars.profile 1622 url = vars.url 1623 newfilename = vars.image_newfilename 1624 if profile == 'False': 1625 profile = False 1626 1627 if newfilename and not url: 1628 # Provide the link to the file in the URL field 1629 url = URL(c="default", f="download", args=newfilename) 1630 query = (table.id == id) 1631 db(query).update(url = url) 1632 1633 if profile: 1634 # Find the pe_id 1635 query = (table.id == id) 1636 pe = db(query).select(table.pe_id, 1637 limitby=(0, 1)).first() 1638 if pe: 1639 pe_id = pe.pe_id 1640 # Set all others for this person as not the Profile picture 1641 query = (table.pe_id == pe_id) & \ 1642 (table.id != id) 1643 db(query).update(profile = False)
1644 1645 # ------------------------------------------------------------------------- 1646 @staticmethod
1647 - def pr_image_onvalidation(form):
1648 """ Image form validation """ 1649 1650 db = current.db 1651 s3db = current.s3db 1652 request = current.request 1653 1654 vars = form.vars 1655 table = s3db.pr_image 1656 image = vars.image 1657 url = vars.url 1658 1659 if not hasattr(image, "file"): 1660 id = request.post_vars.id 1661 if id: 1662 record = db(table.id == id).select(table.image, 1663 limitby=(0, 1)).first() 1664 if record: 1665 image = record.image 1666 1667 if not hasattr(image, "file") and not image and not url: 1668 form.errors.image = \ 1669 form.errors.url = T("Either file upload or image URL required.") 1670 return
1671
1672 # ============================================================================= 1673 -class S3PersonIdentityModel(S3Model):
1674 """ Identities for Persons """ 1675 1676 names = ["pr_identity"] 1677
1678 - def model(self):
1679 1680 T = current.T 1681 db = current.db 1682 request = current.request 1683 s3 = current.response.s3 1684 1685 person_id = self.pr_person_id 1686 1687 UNKNOWN_OPT = current.messages.UNKNOWN_OPT 1688 1689 # --------------------------------------------------------------------- 1690 # Identity 1691 # 1692 # http://docs.oasis-open.org/emergency/edxl-have/cs01/xPIL-types.xsd 1693 # <xs:simpleType name="DocumentTypeList"> 1694 # <xs:enumeration value="Passport"/> 1695 # <xs:enumeration value="DriverLicense"/> 1696 # <xs:enumeration value="CreditCard"/> 1697 # <xs:enumeration value="BankCard"/> 1698 # <xs:enumeration value="KeyCard"/> 1699 # <xs:enumeration value="AccessCard"/> 1700 # <xs:enumeration value="IdentificationCard"/> 1701 # <xs:enumeration value="Certificate"/> 1702 # <xs:enumeration value="MileageProgram"/> 1703 # 1704 pr_id_type_opts = { 1705 1:T("Passport"), 1706 2:T("National ID Card"), 1707 3:T("Driving License"), 1708 #4:T("Credit Card"), 1709 99:T("other") 1710 } 1711 1712 tablename = "pr_identity" 1713 table = self.define_table(tablename, 1714 person_id(label = T("Person"), 1715 ondelete="CASCADE"), 1716 Field("type", "integer", 1717 requires = IS_IN_SET(pr_id_type_opts, zero=None), 1718 default = 1, 1719 label = T("ID type"), 1720 represent = lambda opt: \ 1721 pr_id_type_opts.get(opt, 1722 UNKNOWN_OPT)), 1723 Field("value"), 1724 Field("description"), 1725 Field("country_code", length=4), 1726 Field("ia_name", label = T("Issuing Authority")), 1727 #Field("ia_subdivision"), # Name of issuing authority subdivision 1728 #Field("ia_code"), # Code of issuing authority (if any) 1729 s3.comments(), 1730 *s3.meta_fields()) 1731 1732 # Field configuration 1733 table.value.requires = [IS_NOT_EMPTY(), 1734 IS_NOT_ONE_OF(db, "%s.value" % tablename)] 1735 1736 # CRUD Strings 1737 ADD_IDENTITY = T("Add Identity") 1738 s3.crud_strings[tablename] = Storage( 1739 title_create = ADD_IDENTITY, 1740 title_display = T("Identity Details"), 1741 title_list = T("Known Identities"), 1742 title_update = T("Edit Identity"), 1743 title_search = T("Search Identity"), 1744 subtitle_create = T("Add New Identity"), 1745 subtitle_list = T("Current Identities"), 1746 label_list_button = T("List Identities"), 1747 label_create_button = ADD_IDENTITY, 1748 msg_record_created = T("Identity added"), 1749 msg_record_modified = T("Identity updated"), 1750 msg_record_deleted = T("Identity deleted"), 1751 msg_list_empty = T("No Identities currently registered")) 1752 1753 # Resource configuration 1754 self.configure(tablename, 1755 list_fields=["id", 1756 "type", 1757 "type", 1758 "value", 1759 "country_code", 1760 "ia_name" 1761 ]) 1762 1763 # --------------------------------------------------------------------- 1764 # Return model-global names to response.s3 1765 # 1766 return Storage()
1767
1768 1769 # ============================================================================= 1770 -class S3SavedSearch(S3Model):
1771 """ Saved Searches """ 1772 1773 names = ["pr_save_search"] 1774
1775 - def model(self):
1776 1777 T = current.T 1778 db = current.db 1779 auth = current.auth 1780 request = current.request 1781 s3 = current.response.s3 1782 1783 pr_person = self.table("pr_person") 1784 person_id = s3.pr_person_id 1785 1786 # --------------------------------------------------------------------- 1787 # Saved Searches 1788 # 1789 tablename = "pr_save_search" 1790 table = self.define_table(tablename, 1791 Field("user_id", "integer", 1792 readable = False, 1793 writable = False, 1794 default = auth.user_id), 1795 Field("search_vars","text", 1796 label = T("Search Criteria"), 1797 represent=lambda id:search_vars_represent(id)), 1798 Field("subscribed","boolean", 1799 default=False), 1800 person_id(label = T("Person"), 1801 ondelete="CASCADE", 1802 default = auth.s3_logged_in_person()), 1803 *s3.meta_fields()) 1804 1805 1806 # CRUD Strings 1807 s3.crud_strings[tablename] = Storage( 1808 title_create = T("Save Search"), 1809 title_display = T("Saved Search Details"), 1810 title_list = T("Saved Searches"), 1811 title_update = T("Edit Saved Search"), 1812 title_search = T("Search Saved Searches"), 1813 subtitle_create = T("Add Saved Search"), 1814 subtitle_list = T("Saved Searches"), 1815 label_list_button = T("List Saved Searches"), 1816 label_create_button = T("Save Search"), 1817 label_delete_button = T("Delete Saved Search"), 1818 msg_record_created = T("Saved Search added"), 1819 msg_record_modified = T("Saved Search updated"), 1820 msg_record_deleted = T("Saved Search deleted"), 1821 msg_list_empty = T("No Search saved")) 1822 1823 # Resource configuration 1824 self.configure(tablename, 1825 insertable = False, 1826 editable = False, 1827 listadd = False, 1828 deletable = True, 1829 list_fields=["search_vars"]) 1830 1831 # --------------------------------------------------------------------- 1832 # Return model-global names to response.s3 1833 # 1834 return Storage()
1835
1836 1837 # ============================================================================= 1838 -class S3PersonPresence(S3Model):
1839 """ 1840 Presence Log for Persons 1841 1842 @todo: deprecate 1843 currently still used by CR 1844 """ 1845 1846 names = ["pr_presence", 1847 "pr_trackable_types", 1848 "pr_default_trackable", 1849 "pr_presence_opts", 1850 "pr_presence_conditions", 1851 "pr_default_presence"] 1852
1853 - def model(self):
1854 1855 T = current.T 1856 db = current.db 1857 auth = current.auth 1858 request = current.request 1859 s3 = current.response.s3 1860 1861 person_id = self.pr_person_id 1862 location_id = self.gis_location_id 1863 1864 ADD_LOCATION = current.messages.ADD_LOCATION 1865 UNKNOWN_OPT = current.messages.UNKNOWN_OPT 1866 1867 datetime_represent = S3DateTime.datetime_represent 1868 1869 # Trackable types 1870 pr_trackable_types = { 1871 1:current.T("Person"), # an individual 1872 2:current.T("Group"), # a group 1873 3:current.T("Body"), # a dead body or body part 1874 4:current.T("Object"), # other objects belonging to persons 1875 5:current.T("Organization"), # an organisation 1876 6:current.T("Office"), # an office 1877 } 1878 pr_default_trackable = 1 1879 1880 # Presence conditions 1881 pr_presence_opts = Storage( 1882 SEEN = 1, 1883 TRANSIT = 2, 1884 PROCEDURE = 3, 1885 TRANSITIONAL_PRESENCE = (1, 2, 3), 1886 CHECK_IN = 11, 1887 CONFIRMED = 12, 1888 DECEASED = 13, 1889 LOST = 14, 1890 PERSISTANT_PRESENCE = (11, 12, 13, 14), 1891 TRANSFER = 21, 1892 CHECK_OUT = 22, 1893 ABSENCE = (21, 22), 1894 MISSING = 99 1895 ) 1896 opts = pr_presence_opts 1897 pr_presence_conditions = Storage({ 1898 # Transitional presence conditions: 1899 opts.SEEN: current.T("Seen"), # seen (formerly "found") at location 1900 opts.TRANSIT: current.T("Transit"), # seen at location, between two transfers 1901 opts.PROCEDURE: current.T("Procedure"), # seen at location, undergoing procedure ("Checkpoint") 1902 1903 # Persistant presence conditions: 1904 opts.CHECK_IN: current.T("Check-In"), # arrived at location for accomodation/storage 1905 opts.CONFIRMED: current.T("Confirmed"), # confirmation of stay/storage at location 1906 opts.DECEASED: current.T("Deceased"), # deceased 1907 opts.LOST: current.T("Lost"), # destroyed/disposed at location 1908 1909 # Absence conditions: 1910 opts.TRANSFER: current.T("Transfer"), # Send to another location 1911 opts.CHECK_OUT: current.T("Check-Out"), # Left location for unknown destination 1912 1913 # Missing condition: 1914 opts.MISSING: current.T("Missing"), # Missing (from a "last-seen"-location) 1915 }) 1916 pr_default_presence = 1 1917 1918 resourcename = "presence" 1919 tablename = "pr_presence" 1920 table = self.define_table(tablename, 1921 self.super_link("pe_id", "pr_pentity"), 1922 self.super_link("sit_id", "sit_situation"), 1923 person_id("observer", 1924 label=T("Observer"), 1925 default = auth.s3_logged_in_person(), 1926 comment=pr_person_comment(title=T("Observer"), 1927 comment=T("Person who has actually seen the person/group."), 1928 child="observer")), 1929 Field("shelter_id", "integer", 1930 readable = False, 1931 writable = False), 1932 location_id(widget=S3LocationAutocompleteWidget(), 1933 comment=S3AddResourceLink(c="gis", 1934 f="location", 1935 label=ADD_LOCATION, 1936 title=T("Current Location"), 1937 tooltip=T("The Current Location of the Person/Group, which can be general (for Reporting) or precise (for displaying on a Map). Enter a few characters to search from available locations."))), 1938 Field("location_details", 1939 comment = DIV(_class="tooltip", 1940 _title="%s|%s" % (T("Location Details"), 1941 T("Specific Area (e.g. Building/Room) within the Location that this Person/Group is seen.")))), 1942 Field("datetime", "datetime", 1943 label = T("Date/Time"), 1944 default = request.utcnow, 1945 requires = IS_UTC_DATETIME(allow_future=False), 1946 widget = S3DateTimeWidget(future=0), 1947 represent = lambda val: datetime_represent(val, utc=True)), 1948 Field("presence_condition", "integer", 1949 requires = IS_IN_SET(pr_presence_conditions, 1950 zero=None), 1951 default = pr_default_presence, 1952 label = T("Presence Condition"), 1953 represent = lambda opt: \ 1954 pr_presence_conditions.get(opt, UNKNOWN_OPT)), 1955 Field("proc_desc", 1956 label = T("Procedure"), 1957 comment = DIV(DIV(_class="tooltip", 1958 _title="%s|%s" % (T("Procedure"), 1959 T('Describe the procedure which this record relates to (e.g. "medical examination")'))))), 1960 location_id("orig_id", 1961 label=T("Origin"), 1962 widget = S3LocationAutocompleteWidget(), 1963 comment=S3AddResourceLink(c="gis", 1964 f="location", 1965 label=ADD_LOCATION, 1966 title=T("Origin"), 1967 tooltip=T("The Location the Person has come from, which can be general (for Reporting) or precise (for displaying on a Map). Enter a few characters to search from available locations.")) 1968 ), 1969 1970 location_id("dest_id", 1971 label=T("Destination"), 1972 widget = S3LocationAutocompleteWidget(), 1973 comment=S3AddResourceLink(c="gis", 1974 f="location", 1975 label=ADD_LOCATION, 1976 title=T("Destination"), 1977 tooltip=T("The Location the Person is going to, which can be general (for Reporting) or precise (for displaying on a Map). Enter a few characters to search from available locations.")) 1978 ), 1979 1980 Field("comment"), 1981 Field("closed", "boolean", 1982 default=False, 1983 readable = False, 1984 writable = False), 1985 *s3.meta_fields()) 1986 1987 # CRUD Strings 1988 ADD_LOG_ENTRY = T("Add Log Entry") 1989 s3.crud_strings[tablename] = Storage( 1990 title_create = ADD_LOG_ENTRY, 1991 title_display = T("Log Entry Details"), 1992 title_list = T("Presence Log"), 1993 title_update = T("Edit Log Entry"), 1994 title_search = T("Search Log Entry"), 1995 subtitle_create = T("Add New Log Entry"), 1996 subtitle_list = T("Current Log Entries"), 1997 label_list_button = T("List Log Entries"), 1998 label_create_button = ADD_LOG_ENTRY, 1999 msg_record_created = T("Log entry added"), 2000 msg_record_modified = T("Log entry updated"), 2001 msg_record_deleted = T("Log entry deleted"), 2002 msg_list_empty = T("No Presence Log Entries currently registered")) 2003 2004 # Resource configuration 2005 self.configure(tablename, 2006 super_entity = "sit_situation", 2007 onvalidation = self.presence_onvalidation, 2008 onaccept = self.presence_onaccept, 2009 delete_onaccept = self.presence_onaccept, 2010 list_fields = ["id", 2011 "datetime", 2012 "location_id", 2013 "shelter_id", 2014 "presence_condition", 2015 "orig_id", 2016 "dest_id" 2017 ], 2018 main="time", 2019 extra="location_details") 2020 2021 # --------------------------------------------------------------------- 2022 # Return model-global names to response.s3 2023 # 2024 return Storage( 2025 pr_trackable_types=pr_trackable_types, 2026 pr_default_trackable=pr_default_trackable, 2027 pr_presence_opts=pr_presence_opts, 2028 pr_presence_conditions=pr_presence_conditions, 2029 pr_default_presence=pr_default_presence 2030 )
2031 2032 # ------------------------------------------------------------------------- 2033 @staticmethod
2034 - def presence_onvalidation(form):
2035 """ Presence record validation """ 2036 2037 db = current.db 2038 s3db = current.s3db 2039 s3 = current.response.s3 2040 2041 table = s3db.pr_presence 2042 popts = s3.pr_presence_opts 2043 shelter_table = s3db.cr_shelter 2044 2045 location = form.vars.location_id 2046 shelter = form.vars.shelter_id 2047 2048 if shelter and shelter_table is not None: 2049 set = db(shelter_table.id == shelter) 2050 row = set.select(shelter_table.location_id, limitby=(0, 1)).first() 2051 if row: 2052 location = form.vars.location_id = row.location_id 2053 else: 2054 shelter = None 2055 2056 if location or shelter: 2057 return 2058 2059 condition = form.vars.presence_condition 2060 if condition: 2061 try: 2062 condition = int(condition) 2063 except ValueError: 2064 condition = None 2065 else: 2066 condition = table.presence_condition.default 2067 form.vars.presence_condition = condition 2068 2069 if condition: 2070 if condition in popts.PERSISTANT_PRESENCE or \ 2071 condition in popts.ABSENCE: 2072 if not form.vars.id: 2073 if table.location_id.default or \ 2074 table.shelter_id.default: 2075 return 2076 else: 2077 record = db(table.id == form.vars.id).select(table.location_id, 2078 table.shelter_id, 2079 limitby=(0, 1)).first() 2080 if record and \ 2081 record.location_id or record.shelter_id: 2082 return 2083 else: 2084 return 2085 else: 2086 return 2087 2088 form.errors.location_id = \ 2089 form.errors.shelter_id = T("Either a shelter or a location must be specified") 2090 return
2091 2092 # ------------------------------------------------------------------------- 2093 @staticmethod
2094 - def presence_onaccept(form):
2095 """ 2096 Update the presence log of a person entity 2097 2098 - mandatory to be called as onaccept routine at any modification 2099 of pr_presence records 2100 2101 """ 2102 2103 db = current.db 2104 s3db = current.s3db 2105 s3 = current.response.s3 2106 2107 table = s3db.pr_presence 2108 popts = s3.pr_presence_opts 2109 2110 if isinstance(form, (int, long, str)): 2111 id = form 2112 elif hasattr(form, "vars"): 2113 id = form.vars.id 2114 else: 2115 id = form.id 2116 2117 presence = db(table.id == id).select(table.ALL, limitby=(0,1)).first() 2118 if not presence: 2119 return 2120 else: 2121 condition = presence.presence_condition 2122 2123 pe_id = presence.pe_id 2124 datetime = presence.datetime 2125 if not datetime or not pe_id: 2126 return 2127 2128 this_entity = ((table.pe_id == pe_id) & (table.deleted == False)) 2129 earlier = (table.datetime < datetime) 2130 later = (table.datetime > datetime) 2131 same_place = ((table.location_id == presence.location_id) | 2132 (table.shelter_id == presence.shelter_id)) 2133 is_present = (table.presence_condition.belongs(popts.PERSISTANT_PRESENCE)) 2134 is_absent = (table.presence_condition.belongs(popts.ABSENCE)) 2135 is_missing = (table.presence_condition == popts.MISSING) 2136 2137 if not presence.deleted: 2138 2139 if condition in popts.TRANSITIONAL_PRESENCE: 2140 if presence.closed: 2141 db(table.id == id).update(closed=False) 2142 2143 elif condition in popts.PERSISTANT_PRESENCE: 2144 if not presence.closed: 2145 query = this_entity & earlier & (is_present | is_missing) & \ 2146 (table.closed == False) 2147 db(query).update(closed=True) 2148 2149 query = this_entity & later & \ 2150 (is_present | (is_absent & same_place)) 2151 if db(query).count(): 2152 db(table.id == id).update(closed=True) 2153 2154 elif condition in popts.ABSENCE: 2155 query = this_entity & earlier & is_present & same_place 2156 db(query).update(closed=True) 2157 2158 if not presence.closed: 2159 db(table.id == id).update(closed=True) 2160 2161 if not presence.closed: 2162 2163 # Re-open the last persistant presence if no closing event 2164 query = this_entity & is_present 2165 presence = db(query).select(table.ALL, orderby=~table.datetime, limitby=(0,1)).first() 2166 if presence and presence.closed: 2167 later = (table.datetime > presence.datetime) 2168 query = this_entity & later & is_absent & same_place 2169 if not db(query).count(): 2170 db(table.id == presence.id).update(closed=False) 2171 2172 # Re-open the last missing if no later persistant presence 2173 query = this_entity & is_missing 2174 presence = db(query).select(table.ALL, orderby=~table.datetime, limitby=(0,1)).first() 2175 if presence and presence.closed: 2176 later = (table.datetime > presence.datetime) 2177 query = this_entity & later & is_present 2178 if not db(query).count(): 2179 db(table.id == presence.id).update(closed=False) 2180 2181 pentity = db(db.pr_pentity.pe_id == pe_id).select(db.pr_pentity.instance_type, 2182 limitby=(0,1)).first() 2183 if pentity and pentity.instance_type == "pr_person": 2184 query = this_entity & is_missing & (table.closed == False) 2185 if db(query).count(): 2186 db(db.pr_person.pe_id == pe_id).update(missing = True) 2187 else: 2188 db(db.pr_person.pe_id == pe_id).update(missing = False) 2189 2190 return
2191
2192 # ============================================================================= 2193 -class S3PersonDescription(S3Model):
2194 """ Additional tables for DVI/MPR """ 2195 2196 names = ["pr_note", "pr_physical_description"] 2197
2198 - def model(self):
2199 2200 T = current.T 2201 db = current.db 2202 auth = current.auth 2203 request = current.request 2204 s3 = current.response.s3 2205 2206 person_id = self.pr_person_id 2207 location_id = self.gis_location_id 2208 2209 UNKNOWN_OPT = current.messages.UNKNOWN_OPT 2210 #if deployment_settings.has_module("dvi") or \ 2211 #deployment_settings.has_module("mpr"): 2212 2213 # --------------------------------------------------------------------- 2214 # Note 2215 # 2216 person_status = { 2217 1: T("missing"), 2218 2: T("found"), 2219 3: T("deceased"), 2220 9: T("none") 2221 } 2222 2223 resourcename = "note" 2224 tablename = "pr_note" 2225 table = self.define_table(tablename, 2226 self.super_link("pe_id", "pr_pentity"), 2227 # Reporter 2228 #person_id("reporter"), 2229 Field("confirmed", "boolean", 2230 default=False, 2231 readable=False, 2232 writable=False), 2233 Field("closed", "boolean", 2234 default=False, 2235 readable=False, 2236 writable=False), 2237 Field("status", "integer", 2238 requires=IS_IN_SET(person_status, 2239 zero=None), 2240 default=9, 2241 label=T("Status"), 2242 represent=lambda opt: \ 2243 person_status.get(opt, UNKNOWN_OPT)), 2244 Field("timestmp", "datetime", 2245 label=T("Date/Time"), 2246 requires=[IS_EMPTY_OR(IS_UTC_DATETIME_IN_RANGE())], 2247 widget = S3DateTimeWidget(), 2248 default=request.utcnow), 2249 Field("note_text", "text", 2250 label=T("Text")), 2251 Field("note_contact", "text", 2252 label=T("Contact Info"), 2253 readable=False, 2254 writable=False), 2255 location_id(label=T("Last known location")), 2256 *s3.meta_fields()) 2257 2258 # CRUD strings 2259 ADD_NOTE = T("New Entry") 2260 s3.crud_strings[tablename] = Storage( 2261 title_create = ADD_NOTE, 2262 title_display = T("Journal Entry Details"), 2263 title_list = T("Journal"), 2264 title_update = T("Edit Entry"), 2265 title_search = T("Search Entries"), 2266 subtitle_create = T("Add New Entry"), 2267 subtitle_list = T("Current Entries"), 2268 label_list_button = T("See All Entries"), 2269 label_create_button = ADD_NOTE, 2270 msg_record_created = T("Journal entry added"), 2271 msg_record_modified = T("Journal entry updated"), 2272 msg_record_deleted = T("Journal entry deleted"), 2273 msg_list_empty = T("No entry available")) 2274 2275 # Resource configuration 2276 self.configure(tablename, 2277 list_fields=["id", 2278 "timestmp", 2279 "location_id", 2280 "note_text", 2281 "status"], 2282 editable=False, 2283 onaccept=self.note_onaccept, 2284 ondelete=self.note_onaccept) 2285 2286 # ===================================================================== 2287 # Physical Description 2288 # 2289 pr_race_opts = { 2290 1: T("caucasoid"), 2291 2: T("mongoloid"), 2292 3: T("negroid"), 2293 99: T("other") 2294 } 2295 2296 pr_complexion_opts = { 2297 1: T("light"), 2298 2: T("medium"), 2299 3: T("dark"), 2300 99: T("other") 2301 } 2302 2303 pr_height_opts = { 2304 1: T("short"), 2305 2: T("average"), 2306 3: T("tall") 2307 } 2308 2309 pr_weight_opts = { 2310 1: T("slim"), 2311 2: T("average"), 2312 3: T("fat") 2313 } 2314 2315 # http://docs.oasis-open.org/emergency/edxl-have/cs01/xPIL-types.xsd 2316 pr_blood_type_opts = ("A+", "A-", "B+", "B-", "AB+", "AB-", "0+", "0-") 2317 2318 pr_eye_color_opts = { 2319 1: T("blue"), 2320 2: T("grey"), 2321 3: T("green"), 2322 4: T("brown"), 2323 5: T("black"), 2324 99: T("other") 2325 } 2326 2327 pr_hair_color_opts = { 2328 1: T("blond"), 2329 2: T("brown"), 2330 3: T("black"), 2331 4: T("red"), 2332 5: T("grey"), 2333 6: T("white"), 2334 99: T("see comment") 2335 } 2336 2337 pr_hair_style_opts = { 2338 1: T("straight"), 2339 2: T("wavy"), 2340 3: T("curly"), 2341 99: T("see comment") 2342 } 2343 2344 pr_hair_length_opts = { 2345 1: T("short<6cm"), 2346 2: T("medium<12cm"), 2347 3: T("long>12cm"), 2348 4: T("shaved"), 2349 99: T("see comment") 2350 } 2351 2352 pr_hair_baldness_opts = { 2353 1: T("forehead"), 2354 2: T("sides"), 2355 3: T("tonsure"), 2356 4: T("total"), 2357 99: T("see comment") 2358 } 2359 2360 pr_facial_hair_type_opts = { 2361 1: T("none"), 2362 2: T("Moustache"), 2363 3: T("Goatee"), 2364 4: T("Whiskers"), 2365 5: T("Full beard"), 2366 99: T("see comment") 2367 } 2368 2369 pr_facial_hair_length_opts = { 2370 1: T("short"), 2371 2: T("medium"), 2372 3: T("long"), 2373 4: T("shaved") 2374 } 2375 2376 # This set is suitable for use in the US 2377 pr_ethnicity_opts = [ 2378 "American Indian", 2379 "Alaskan", 2380 "Asian", 2381 "African American", 2382 "Hispanic or Latino", 2383 "Native Hawaiian", 2384 "Pacific Islander", 2385 "Two or more", 2386 "Unspecified", 2387 "White" 2388 ] 2389 2390 resourcename = "physical_description" 2391 tablename = "pr_physical_description" 2392 table = self.define_table(tablename, 2393 self.super_link("pe_id", "pr_pentity"), 2394 # Race and complexion 2395 Field("race", "integer", 2396 requires = IS_EMPTY_OR(IS_IN_SET(pr_race_opts)), 2397 label = T("Race"), 2398 represent = lambda opt: \ 2399 pr_race_opts.get(opt, UNKNOWN_OPT)), 2400 Field("complexion", "integer", 2401 requires = IS_EMPTY_OR(IS_IN_SET(pr_complexion_opts)), 2402 label = T("Complexion"), 2403 represent = lambda opt: \ 2404 pr_complexion_opts.get(opt, UNKNOWN_OPT)), 2405 Field("ethnicity", 2406 #requires=IS_NULL_OR(IS_IN_SET(pr_ethnicity_opts)), 2407 length=64), # Mayon Compatibility 2408 2409 # Height and weight 2410 Field("height", "integer", 2411 requires = IS_EMPTY_OR(IS_IN_SET(pr_height_opts)), 2412 label = T("Height"), 2413 represent = lambda opt: \ 2414 pr_height_opts.get(opt, UNKNOWN_OPT)), 2415 Field("height_cm", "integer", 2416 requires = IS_NULL_OR(IS_INT_IN_RANGE(0, 300)), 2417 label = T("Height (cm)")), 2418 Field("weight", "integer", 2419 requires = IS_EMPTY_OR(IS_IN_SET(pr_weight_opts)), 2420 label = T("Weight"), 2421 represent = lambda opt: \ 2422 pr_weight_opts.get(opt, UNKNOWN_OPT)), 2423 Field("weight_kg", "integer", 2424 requires = IS_NULL_OR(IS_INT_IN_RANGE(0, 500)), 2425 label = T("Weight (kg)")), 2426 2427 # Blood type, eye color 2428 Field("blood_type", 2429 requires = IS_EMPTY_OR(IS_IN_SET(pr_blood_type_opts)), 2430 label = T("Blood Type (AB0)"), 2431 represent = lambda opt: opt or UNKNOWN_OPT), 2432 Field("eye_color", "integer", 2433 requires = IS_EMPTY_OR(IS_IN_SET(pr_eye_color_opts)), 2434 label = T("Eye Color"), 2435 represent = lambda opt: \ 2436 pr_eye_color_opts.get(opt, UNKNOWN_OPT)), 2437 2438 # Hair of the head 2439 Field("hair_color", "integer", 2440 requires = IS_EMPTY_OR(IS_IN_SET(pr_hair_color_opts)), 2441 label = T("Hair Color"), 2442 represent = lambda opt: \ 2443 pr_hair_color_opts.get(opt, UNKNOWN_OPT)), 2444 Field("hair_style", "integer", 2445 requires = IS_EMPTY_OR(IS_IN_SET(pr_hair_style_opts)), 2446 label = T("Hair Style"), 2447 represent = lambda opt: \ 2448 pr_hair_style_opts.get(opt, UNKNOWN_OPT)), 2449 Field("hair_length", "integer", 2450 requires = IS_EMPTY_OR(IS_IN_SET(pr_hair_length_opts)), 2451 label = T("Hair Length"), 2452 represent = lambda opt: \ 2453 pr_hair_length_opts.get(opt, UNKNOWN_OPT)), 2454 Field("hair_baldness", "integer", 2455 requires = IS_EMPTY_OR(IS_IN_SET(pr_hair_baldness_opts)), 2456 label = T("Baldness"), 2457 represent = lambda opt: \ 2458 pr_hair_baldness_opts.get(opt, UNKNOWN_OPT)), 2459 Field("hair_comment"), 2460 2461 # Facial hair 2462 Field("facial_hair_type", "integer", 2463 requires = IS_EMPTY_OR(IS_IN_SET(pr_facial_hair_type_opts)), 2464 label = T("Facial hair, type"), 2465 represent = lambda opt: \ 2466 pr_facial_hair_type_opts.get(opt, UNKNOWN_OPT)), 2467 Field("facial_hair_color", "integer", 2468 requires = IS_EMPTY_OR(IS_IN_SET(pr_hair_color_opts)), 2469 label = T("Facial hair, color"), 2470 represent = lambda opt: \ 2471 pr_hair_color_opts.get(opt, UNKNOWN_OPT)), 2472 Field("facial_hair_length", "integer", 2473 requires = IS_EMPTY_OR(IS_IN_SET(pr_facial_hair_length_opts)), 2474 label = T("Facial hear, length"), 2475 represent = lambda opt: \ 2476 pr_facial_hair_length_opts.get(opt, UNKNOWN_OPT)), 2477 Field("facial_hair_comment"), 2478 2479 # Body hair and skin marks 2480 Field("body_hair"), 2481 Field("skin_marks", "text"), 2482 2483 # Medical Details: scars, amputations, implants 2484 Field("medical_conditions", "text"), 2485 2486 # Other details 2487 Field("other_details", "text"), 2488 2489 s3.comments(), 2490 *s3.meta_fields()) 2491 2492 # Field configuration 2493 table.height_cm.comment = DIV(DIV(_class="tooltip", 2494 _title="%s|%s" % (T("Height"), 2495 T("The body height (crown to heel) in cm.")))) 2496 table.weight_kg.comment = DIV(DIV(_class="tooltip", 2497 _title="%s|%s" % (T("Weight"), 2498 T("The weight in kg.")))) 2499 2500 table.pe_id.readable = False 2501 table.pe_id.writable = False
2502 2503 # CRUD Strings 2504 # ? 2505 2506 # Resource Configuration 2507 # ? 2508 2509 # ------------------------------------------------------------------------- 2510 @staticmethod
2511 - def note_onaccept(form):
2512 """ Update missing status for person """ 2513 2514 db = current.db 2515 s3db = current.s3db 2516 2517 pe_table = s3db.pr_pentity 2518 ntable = s3db.pr_note 2519 ptable = s3db.pr_person 2520 2521 if isinstance(form, (int, long, str)): 2522 _id = form 2523 elif hasattr(form, "vars"): 2524 _id = form.vars.id 2525 else: 2526 _id = form.id 2527 2528 note = ntable[_id] 2529 if not note: 2530 return 2531 2532 query = (ntable.pe_id == note.pe_id) & \ 2533 (ntable.deleted != True) 2534 mq = query & ntable.status == 1 2535 fq = query & ntable.status.belongs((2, 3)) 2536 mr = db(mq).select(ntable.id, 2537 ntable.timestmp, 2538 orderby=~ntable.timestmp, 2539 limitby=(0, 1)).first() 2540 fr = db(fq).select(ntable.id, 2541 ntable.timestmp, 2542 orderby=~ntable.timestmp, 2543 limitby=(0, 1)).first() 2544 missing = False 2545 if mr and not fr or fr.timestmp < mr.timestmp: 2546 missing = True 2547 query = ptable.pe_id == note.pe_id 2548 db(query).update(missing=missing) 2549 if note.deleted: 2550 try: 2551 location_id = form.location_id 2552 except: 2553 pass 2554 else: 2555 ttable = s3db.sit_presence 2556 query = (ptable.pe_id == note.pe_id) & \ 2557 (ttable.uuid == ptable.uuid) & \ 2558 (ttable.location_id == location_id) & \ 2559 (ttable.timestmp == note.timestmp) 2560 if note.location_id: 2561 tracker = S3Tracker() 2562 tracker(query).set_location(note.location_id, 2563 timestmp=note.timestmp) 2564 return
2565
2566 # ============================================================================= 2567 # Representation Methods 2568 # ============================================================================= 2569 # 2570 -def pr_pentity_represent(id, show_label=True, default_label="[No ID Tag]"):
2571 """ Represent a Person Entity in option fields or list views """ 2572 2573 T = current.T 2574 db = current.db 2575 s3db = current.s3db 2576 2577 if not id: 2578 return current.messages.NONE 2579 2580 pe_str = T("None (no such record)") 2581 2582 pe_table = s3db.pr_pentity 2583 pe = db(pe_table.pe_id == id).select(pe_table.instance_type, 2584 pe_table.pe_label, 2585 limitby=(0, 1)).first() 2586 if not pe: 2587 return pe_str 2588 2589 instance_type = pe.instance_type 2590 instance_type_nice = pe_table.instance_type.represent(instance_type) 2591 2592 table = s3db.table(instance_type, None) 2593 if not table: 2594 return pe_str 2595 2596 label = pe.pe_label or default_label 2597 2598 if instance_type == "pr_person": 2599 person = db(table.pe_id == id).select( 2600 table.first_name, table.middle_name, table.last_name, 2601 limitby=(0, 1)).first() 2602 if person: 2603 if show_label: 2604 pe_str = "%s %s (%s)" % (s3_fullname(person), 2605 label, instance_type_nice) 2606 else: 2607 pe_str = "%s (%s)" % (s3_fullname(person), 2608 instance_type_nice) 2609 elif instance_type == "pr_group": 2610 group = db(table.pe_id == id).select(table.name, 2611 limitby=(0, 1)).first() 2612 if group: 2613 pe_str = "%s (%s)" % (group.name, instance_type_nice) 2614 elif instance_type == "org_organisation": 2615 organisation = db(table.pe_id == id).select(table.name, 2616 limitby=(0, 1)).first() 2617 if organisation: 2618 pe_str = "%s (%s)" % (organisation.name, instance_type_nice) 2619 elif instance_type == "org_office": 2620 office = db(table.pe_id == id).select(table.name, 2621 limitby=(0, 1)).first() 2622 if office: 2623 pe_str = "%s (%s)" % (office.name, instance_type_nice) 2624 else: 2625 pe_str = "[%s] (%s)" % (label, 2626 instance_type_nice) 2627 return pe_str
2628
2629 # ============================================================================= 2630 -def pr_person_represent(id):
2631 """ Representation """ 2632 2633 if not id: 2634 return current.messages.NONE 2635 2636 db = current.db 2637 s3db = current.s3db 2638 cache = current.cache 2639 2640 table = s3db.pr_person 2641 2642 def _represent(id): 2643 if isinstance(id, Row): 2644 person = id 2645 id = person.id 2646 else: 2647 person = db(table.id == id).select(table.first_name, 2648 table.middle_name, 2649 table.last_name, 2650 limitby=(0, 1)).first() 2651 if person: 2652 return s3_fullname(person) 2653 else: 2654 return None
2655 2656 name = cache.ram("pr_person_%s" % id, 2657 lambda: _represent(id), time_expire=10) 2658 return name 2659
2660 # ============================================================================= 2661 -def pr_person_comment(title=None, comment=None, caller=None, child=None):
2662 2663 T = current.T 2664 if title is None: 2665 title = T("Person") 2666 if comment is None: 2667 comment = T("Type the first few characters of one of the Person's names.") 2668 if child is None: 2669 child = "person_id" 2670 return S3AddResourceLink(c="pr", f="person", 2671 vars=dict(caller=caller, child=child), 2672 title=current.messages.ADD_PERSON, 2673 tooltip="%s|%s" % (title, comment))
2674
2675 # ============================================================================= 2676 -def pr_rheader(r, tabs=[]):
2677 """ 2678 Person Registry resource headers 2679 - used in PR, HRM, DVI, MPR, MSG, VOL 2680 """ 2681 2682 T = current.T 2683 db = current.db 2684 s3db = current.s3db 2685 gis = current.gis 2686 s3 = current.response.s3 2687 2688 tablename, record = s3_rheader_resource(r) 2689 2690 if r.representation == "html": 2691 rheader_tabs = s3_rheader_tabs(r, tabs) 2692 2693 if tablename == "pr_person": 2694 person = record 2695 if person: 2696 s3 = current.response.s3 2697 ptable = r.table 2698 itable = s3db.pr_image 2699 query = (itable.pe_id == record.pe_id) & \ 2700 (itable.profile == True) 2701 image = db(query).select(itable.image, 2702 limitby=(0, 1)).first() 2703 if image: 2704 image = TD(itable.image.represent(image.image), 2705 _rowspan=3) 2706 else: 2707 image = "" 2708 rheader = DIV(TABLE( 2709 TR(TH("%s: " % T("Name")), 2710 s3_fullname(person), 2711 TH("%s: " % T("ID Tag Number")), 2712 "%(pe_label)s" % person, 2713 image), 2714 TR(TH("%s: " % T("Date of Birth")), 2715 "%s" % (person.date_of_birth or T("unknown")), 2716 TH("%s: " % T("Gender")), 2717 "%s" % s3.pr_gender_opts.get(person.gender, T("unknown"))), 2718 2719 TR(TH("%s: " % T("Nationality")), 2720 "%s" % (gis.get_country(person.nationality, key_type="code") or T("unknown")), 2721 TH("%s: " % T("Age Group")), 2722 "%s" % s3.pr_age_group_opts.get(person.age_group, T("unknown"))), 2723 2724 ), rheader_tabs) 2725 return rheader 2726 2727 elif tablename == "pr_group": 2728 group = record 2729 if group: 2730 table = s3db.pr_group_membership 2731 query = (table.group_id == record.id) & \ 2732 (table.group_head == True) 2733 leader = db(query).select(table.person_id, 2734 limitby=(0, 1)).first() 2735 if leader: 2736 leader = s3_fullname(leader.person_id) 2737 else: 2738 leader = "" 2739 rheader = DIV(TABLE( 2740 TR(TH("%s: " % T("Name")), 2741 group.name, 2742 TH("%s: " % T("Leader")) if leader else "", 2743 leader), 2744 TR(TH("%s: " % T("Description")), 2745 group.description or "", 2746 TH(""), 2747 "") 2748 ), rheader_tabs) 2749 return rheader 2750 2751 return None
2752
2753 # ============================================================================= 2754 # Custom Resource Methods 2755 # ============================================================================= 2756 # 2757 -def pr_contacts(r, **attr):
2758 """ 2759 Custom Method to provide the details for the Person's Contacts Tab: 2760 - provides a single view on: 2761 Addresses (pr_address) 2762 Contacts (pr_contact) 2763 Emergency Contacts 2764 2765 @ToDo: Fix Map in Address' LocationSelector 2766 @ToDo: Allow Address Create's LocationSelector to work in Debug mode 2767 """ 2768 2769 from itertools import groupby 2770 2771 if r.http != "GET": 2772 r.error(405, current.manager.ERROR.BAD_METHOD) 2773 2774 T = current.T 2775 db = current.db 2776 s3db = current.s3db 2777 2778 person = r.record 2779 2780 # Addresses 2781 atable = s3db.pr_address 2782 query = (atable.pe_id == person.pe_id) 2783 addresses = db(query).select(atable.id, 2784 atable.type, 2785 atable.building_name, 2786 atable.address, 2787 atable.postcode, 2788 orderby=atable.type) 2789 2790 address_groups = {} 2791 for key, group in groupby(addresses, lambda a: a.type): 2792 address_groups[key] = list(group) 2793 2794 address_wrapper = DIV(H2(T("Addresses")), 2795 DIV(A(T("Add"), _class="addBtn", _id="address-add"), 2796 _class="margin")) 2797 2798 items = address_groups.items() 2799 opts = s3db.pr_address_type_opts 2800 for address_type, details in items: 2801 address_wrapper.append(H3(opts[address_type])) 2802 for detail in details: 2803 building_name = detail.building_name or "" 2804 if building_name: 2805 building_name = "%s, " % building_name 2806 address = detail.address or "" 2807 if address: 2808 address = "%s, " % address 2809 postcode = detail.postcode or "" 2810 address_wrapper.append(P( 2811 SPAN("%s%s%s" % (building_name, 2812 address, 2813 postcode)), 2814 A(T("Edit"), _class="editBtn fright"), 2815 _id="address-%s" % detail.id, 2816 _class="address", 2817 )) 2818 2819 # Contacts 2820 ctable = s3db.pr_contact 2821 query = (ctable.pe_id == person.pe_id) 2822 contacts = db(query).select(ctable.id, 2823 ctable.value, 2824 ctable.contact_method, 2825 orderby=ctable.contact_method) 2826 2827 contact_groups = {} 2828 for key, group in groupby(contacts, lambda c: c.contact_method): 2829 contact_groups[key] = list(group) 2830 2831 contacts_wrapper = DIV(H2(T("Contacts")), 2832 DIV(A(T("Add"), _class="addBtn", _id="contact-add"), 2833 _class="margin")) 2834 2835 2836 items = contact_groups.items() 2837 opts = current.msg.CONTACT_OPTS 2838 for contact_type, details in items: 2839 contacts_wrapper.append(H3(opts[contact_type])) 2840 for detail in details: 2841 contacts_wrapper.append(P( 2842 SPAN(detail.value), 2843 A(T("Edit"), _class="editBtn fright"), 2844 _id="contact-%s" % detail.id, 2845 _class="contact", 2846 )) 2847 2848 # Emergency Contacts 2849 etable = s3db.pr_contact_emergency 2850 query = (etable.pe_id == person.pe_id) & \ 2851 (etable.deleted == False) 2852 emergency = db(query).select(etable.id, 2853 etable.name, 2854 etable.relationship, 2855 etable.phone) 2856 2857 emergency_wrapper = DIV(H2(T("Emergency Contacts")), 2858 DIV(A(T("Add"), _class="addBtn", _id="emergency-add"), 2859 _class="margin")) 2860 2861 for contact in emergency: 2862 name = contact.name or "" 2863 if name: 2864 name = "%s, "% name 2865 relationship = contact.relationship or "" 2866 if relationship: 2867 relationship = "%s, "% relationship 2868 emergency_wrapper.append(P( 2869 SPAN("%s%s%s" % (name, relationship, contact.phone)), 2870 A(T("Edit"), _class="editBtn fright"), 2871 _id="emergency-%s" % contact.id, 2872 _class="emergency", 2873 )) 2874 2875 # Overall content 2876 content = DIV(address_wrapper, 2877 contacts_wrapper, 2878 emergency_wrapper, 2879 _class="contacts-wrapper") 2880 2881 # Add the javascript 2882 response = current.response 2883 s3 = response.s3 2884 s3.scripts.append(URL(c="static", f="scripts", 2885 args=["S3", "s3.contacts.js"])) 2886 s3.js_global.append("personId = %s;" % person.id); 2887 2888 # Custom View 2889 response.view = "pr/contacts.html" 2890 2891 # RHeader for consistency 2892 rheader = attr.get("rheader", None) 2893 if callable(rheader): 2894 rheader = rheader(r) 2895 2896 return dict( 2897 title = T("Contacts"), 2898 rheader = rheader, 2899 content = content, 2900 )
2901
2902 # ============================================================================= 2903 -def pr_profile(r, **attr):
2904 """ 2905 Custom Method to provide the auth_user profile as a Tab of the Person 2906 @ToDo: Complete this (currently unfinished) 2907 """ 2908 2909 if r.http != "GET": 2910 r.error(405, current.manager.ERROR.BAD_METHOD) 2911 2912 T = current.T 2913 db = current.db 2914 s3db = current.s3db 2915 2916 person = r.record 2917 2918 # Profile 2919 ltable = s3db.pr_person_user 2920 query = (ltable.pe_id == person.pe_id) 2921 profile = db(query).select(limitby=(0, 1)).first() 2922 2923 form = current.auth() 2924 2925 # Custom View 2926 response.view = "pr/profile.html" 2927 2928 # RHeader for consistency 2929 rheader = s3db.hrm_rheader(r) 2930 2931 return dict( 2932 title = T("Profile"), 2933 rheader = rheader, 2934 form = form, 2935 )
2936
2937 # ============================================================================= 2938 # Hierarchy Manipulation 2939 # ============================================================================= 2940 # 2941 -def pr_update_affiliations(table, record):
2942 """ 2943 Update OU affiliations related to this record 2944 2945 @param table: the table 2946 @param record: the record 2947 """ 2948 2949 if hasattr(table, "_tablename"): 2950 rtype = table._tablename 2951 else: 2952 rtype = table 2953 2954 db = current.db 2955 s3db = current.s3db 2956 2957 if rtype == "hrm_human_resource": 2958 2959 # Get the HR record 2960 htable = s3db.hrm_human_resource 2961 if not isinstance(record, Row): 2962 record = db(htable.id == record).select(htable.ALL, 2963 limitby=(0, 1)).first() 2964 if not record: 2965 return 2966 2967 # Find the person_ids to update 2968 update = pr_human_resource_update_affiliations 2969 person_id = None 2970 if record.deleted_fk: 2971 try: 2972 person_id = json.loads(record.deleted_fk)["person_id"] 2973 except: 2974 pass 2975 if person_id: 2976 update(person_id) 2977 if person_id != record.person_id: 2978 person_id = record.person_id 2979 if person_id: 2980 update(person_id) 2981 2982 elif rtype == "pr_group_membership": 2983 2984 mtable = s3db.pr_group_membership 2985 if not isinstance(record, Row): 2986 record = db(mtable.id == record).select(mtable.ALL, 2987 limitby=(0, 1)).first() 2988 if not record: 2989 return 2990 pr_group_update_affiliations(record) 2991 2992 elif rtype == "org_organisation_branch": 2993 2994 ltable = s3db.org_organisation_branch 2995 if not isinstance(record, Row): 2996 record = db(ltable.id == record).select(ltable.ALL, 2997 limitby=(0, 1)).first() 2998 if not record: 2999 return 3000 pr_organisation_update_affiliations(record) 3001 3002 elif rtype == "org_site": 3003 3004 pr_site_update_affiliations(record) 3005 3006 return
3007
3008 # ============================================================================= 3009 -def pr_organisation_update_affiliations(record):
3010 """ 3011 Update affiliations for a branch organisation 3012 3013 @param record: the org_organisation_branch record 3014 """ 3015 3016 db = current.db 3017 s3db = current.s3db 3018 3019 if record.deleted and record.deleted_fk: 3020 try: 3021 fk = json.loads(record.deleted_fk) 3022 branch_id = fk["branch_id"] 3023 except: 3024 return 3025 else: 3026 branch_id = record.branch_id 3027 3028 BRANCHES = "Branches" 3029 3030 otable = s3db.org_organisation 3031 btable = otable.with_alias("branch") 3032 ltable = s3db.org_organisation_branch 3033 rtable = s3db.pr_role 3034 atable = s3db.pr_affiliation 3035 etable = s3db.pr_pentity 3036 3037 o = otable._tablename 3038 b = btable._tablename 3039 r = rtable._tablename 3040 3041 # Get current memberships 3042 query = (ltable.branch_id == branch_id) & \ 3043 (ltable.deleted != True) 3044 left = [otable.on(ltable.organisation_id == otable.id), 3045 btable.on(ltable.branch_id == btable.id)] 3046 rows = db(query).select(otable.pe_id, btable.pe_id, left=left) 3047 current_memberships = [(row[o].pe_id, row[b].pe_id) for row in rows] 3048 3049 # Get current affiliations 3050 query = (rtable.deleted != True) & \ 3051 (rtable.role == BRANCHES) & \ 3052 (rtable.pe_id == etable.pe_id) & \ 3053 (etable.instance_type == o) & \ 3054 (atable.deleted != True) & \ 3055 (atable.role_id == rtable.id) & \ 3056 (atable.pe_id == btable.pe_id) & \ 3057 (btable.id == branch_id) 3058 rows = db(query).select(rtable.pe_id, btable.pe_id) 3059 current_affiliations = [(row[r].pe_id, row[b].pe_id) for row in rows] 3060 3061 # Remove all affiliations which are not current memberships 3062 for a in current_affiliations: 3063 org, branch = a 3064 if a not in current_memberships: 3065 pr_remove_affiliation(org, branch, role=BRANCHES) 3066 else: 3067 current_memberships.remove(a) 3068 # Add affiliations for all new memberships 3069 for m in current_memberships: 3070 org, branch = m 3071 pr_add_affiliation(org, branch, role=BRANCHES, role_type=OU) 3072 return
3073
3074 # ============================================================================= 3075 -def pr_group_update_affiliations(record):
3076 """ 3077 Update affiliations for group memberships, currently this makes 3078 all members of a group organisational units of the group. 3079 3080 @param record: the pr_membership record 3081 """ 3082 3083 db = current.db 3084 s3db = current.s3db 3085 3086 MEMBERS = "Members" 3087 3088 if record.deleted and record.deleted_fk: 3089 try: 3090 fk = json.loads(record.deleted_fk) 3091 person_id = fk["person_id"] 3092 except: 3093 return 3094 else: 3095 person_id = record.person_id 3096 3097 ptable = s3db.pr_person 3098 gtable = s3db.pr_group 3099 mtable = s3db.pr_group_membership 3100 rtable = s3db.pr_role 3101 atable = s3db.pr_affiliation 3102 etable = s3db.pr_pentity 3103 3104 g = gtable._tablename 3105 r = rtable._tablename 3106 p = ptable._tablename 3107 3108 # Get current memberships 3109 query = (mtable.person_id == person_id) & \ 3110 (mtable.deleted != True) 3111 left = [ptable.on(mtable.person_id == ptable.id), 3112 gtable.on(mtable.group_id == gtable.id)] 3113 rows = db(query).select(ptable.pe_id, gtable.pe_id, left=left) 3114 current_memberships = [(row[g].pe_id, row[p].pe_id) for row in rows] 3115 3116 # Get current affiliations 3117 query = (rtable.deleted != True) & \ 3118 (rtable.role == MEMBERS) & \ 3119 (rtable.pe_id == etable.pe_id) & \ 3120 (etable.instance_type == g) & \ 3121 (atable.deleted != True) & \ 3122 (atable.role_id == rtable.id) & \ 3123 (atable.pe_id == ptable.pe_id) & \ 3124 (ptable.id == person_id) 3125 rows = db(query).select(ptable.pe_id, rtable.pe_id) 3126 current_affiliations = [(row[r].pe_id, row[p].pe_id) for row in rows] 3127 3128 # Remove all affiliations which are not current memberships 3129 for a in current_affiliations: 3130 group, person = a 3131 if a not in current_memberships: 3132 pr_remove_affiliation(group, person, role=MEMBERS) 3133 else: 3134 current_memberships.remove(a) 3135 # Add affiliations for all new memberships 3136 for m in current_memberships: 3137 group, person = m 3138 pr_add_affiliation(group, person, role=MEMBERS, role_type=OU) 3139 return
3140
3141 # ============================================================================= 3142 -def pr_site_update_affiliations(record):
3143 """ 3144 Update the affiliations of an org_site instance 3145 3146 @param record: the org_site instance record 3147 """ 3148 3149 db = current.db 3150 s3db = current.s3db 3151 3152 SITES = "Sites" 3153 3154 otable = s3db.org_organisation 3155 stable = s3db.org_site 3156 rtable = s3db.pr_role 3157 ptable = s3db.pr_pentity 3158 atable = s3db.pr_affiliation 3159 3160 o_pe_id = None 3161 s_pe_id = record.pe_id 3162 3163 organisation_id = record.organisation_id 3164 if organisation_id: 3165 org = db(otable.id == organisation_id).select(otable.pe_id, 3166 limitby=(0, 1)).first() 3167 if org: 3168 o_pe_id = org.pe_id 3169 if s_pe_id: 3170 query = (atable.deleted != True) & \ 3171 (atable.pe_id == s_pe_id) & \ 3172 (rtable.deleted != True) & \ 3173 (rtable.id == atable.role_id) & \ 3174 (rtable.role == SITES) & \ 3175 (ptable.pe_id == rtable.pe_id) & \ 3176 (ptable.instance_type == str(otable)) 3177 rows = db(query).select(rtable.pe_id) 3178 seen = False 3179 for row in rows: 3180 if o_pe_id == None or o_pe_id != row.pe_id: 3181 pr_remove_affiliation(row.pe_id, s_pe_id, role=SITES) 3182 elif o_pe_id == row.pe_id: 3183 seen = True 3184 if o_pe_id and not seen: 3185 pr_add_affiliation(o_pe_id, s_pe_id, role=SITES, role_type=OU) 3186 return
3187
3188 # ============================================================================= 3189 -def pr_human_resource_update_affiliations(person_id):
3190 """ 3191 Update all affiliations related to the HR records of a person 3192 3193 @param person_id: the person record ID 3194 """ 3195 3196 db = current.db 3197 s3db = current.s3db 3198 3199 STAFF = "Staff" 3200 VOLUNTEER = "Volunteer" 3201 3202 update = pr_human_resource_update_affiliations 3203 3204 etable = s3db.pr_pentity 3205 ptable = s3db.pr_person 3206 rtable = s3db.pr_role 3207 atable = s3db.pr_affiliation 3208 htable = s3db.hrm_human_resource 3209 otable = s3db.org_organisation 3210 stable = s3db.org_site 3211 3212 h = htable._tablename 3213 s = stable._tablename 3214 o = otable._tablename 3215 r = rtable._tablename 3216 e = etable._tablename 3217 3218 # Get the PE-ID for this person 3219 pe_id = s3db.pr_get_pe_id("pr_person", person_id) 3220 3221 # Get all current HR records 3222 query = (htable.person_id == person_id) & \ 3223 (htable.status == 1) & \ 3224 (htable.type.belongs((1,2))) & \ 3225 (htable.deleted != True) 3226 left = [otable.on(htable.organisation_id == otable.id), 3227 stable.on(htable.site_id == stable.site_id)] 3228 rows = db(query).select(htable.site_id, 3229 htable.type, 3230 otable.pe_id, 3231 stable.uuid, 3232 stable.instance_type, 3233 left=left) 3234 3235 # Extract all master PE's 3236 masters = {STAFF:[], VOLUNTEER:[]} 3237 sites = Storage() 3238 for row in rows: 3239 if row[h].type == 1: 3240 role = STAFF 3241 site_id = row[h].site_id 3242 site_pe_id = None 3243 if site_id and site_id not in sites: 3244 itable = s3db.table(row[s].instance_type, None) 3245 if itable and "pe_id" in itable.fields: 3246 q = itable.site_id == site_id 3247 site = db(q).select(itable.pe_id, 3248 limitby=(0, 1)).first() 3249 if site: 3250 site_pe_id = sites[site_id] = site.pe_id 3251 else: 3252 site_pe_id = sites[site_id] 3253 if site_pe_id and site_pe_id not in masters[role]: 3254 masters[role].append(site_pe_id) 3255 elif row[h].type == 2: 3256 role = VOLUNTEER 3257 else: 3258 continue 3259 org_pe_id = row[o].pe_id 3260 if org_pe_id and org_pe_id not in masters[role]: 3261 masters[role].append(org_pe_id) 3262 3263 # Get all current affiliations 3264 query = (ptable.id == person_id) & \ 3265 (atable.deleted != True) & \ 3266 (atable.pe_id == ptable.pe_id) & \ 3267 (rtable.deleted != True) & \ 3268 (rtable.id == atable.role_id) & \ 3269 (rtable.role.belongs((STAFF, VOLUNTEER))) & \ 3270 (etable.pe_id == rtable.pe_id) & \ 3271 (etable.instance_type.belongs((o, s))) 3272 affiliations = db(query).select(rtable.id, 3273 rtable.pe_id, 3274 rtable.role, 3275 etable.instance_type) 3276 3277 # Remove all affiliations which are not in masters 3278 for a in affiliations: 3279 pe = a[r].pe_id 3280 role = a[r].role 3281 if role in masters: 3282 if pe not in masters[role]: 3283 pr_remove_affiliation(pe, pe_id, role=role) 3284 else: 3285 masters[role].remove(pe) 3286 3287 # Add affiliations to all masters which are not in current affiliations 3288 for role in masters: 3289 if role == VOLUNTEER: 3290 role_type = OTHER_ROLE 3291 else: 3292 role_type = OU 3293 for m in masters[role]: 3294 pr_add_affiliation(m, pe_id, role=role, role_type=role_type) 3295 3296 return
3297
3298 # ============================================================================= 3299 -def pr_add_affiliation(master, affiliate, role=None, role_type=OU):
3300 """ 3301 Add a new affiliation record 3302 3303 @param master: the master entity, either as PE-ID or as tuple 3304 (instance_type, instance_id) 3305 @param affiliate: the affiliated entity, either as PE-ID or as tuple 3306 (instance_type, instance_id) 3307 @param role: the role to add the affiliate to (will be created if it 3308 doesn't yet exist) 3309 @param role_type: the type of the role, defaults to OU 3310 """ 3311 3312 db = current.db 3313 s3db = current.s3db 3314 3315 if not role: 3316 return 3317 3318 master_pe = pr_get_pe_id(master) 3319 affiliate_pe = pr_get_pe_id(affiliate) 3320 3321 role_id = None 3322 if master_pe and affiliate_pe: 3323 rtable = s3db.pr_role 3324 query = (rtable.pe_id == master_pe) & \ 3325 (rtable.role == role) & \ 3326 (rtable.deleted != True) 3327 row = db(query).select(limitby=(0, 1)).first() 3328 if not row: 3329 data = {"pe_id": master_pe, 3330 "role": role, 3331 "role_type": role_type} 3332 role_id = rtable.insert(**data) 3333 else: 3334 role_id = row.id 3335 if role_id: 3336 pr_add_to_role(role_id, affiliate_pe) 3337 return role_id
3338
3339 # ============================================================================= 3340 -def pr_remove_affiliation(master, affiliate, role=None):
3341 """ 3342 Remove affiliation records 3343 3344 @param master: the master entity, either as PE-ID or as tuple 3345 (instance_type, instance_id), if this is None, then 3346 all affiliations with all entities will be removed 3347 @param affiliate: the affiliated entity, either as PE-ID or as tuple 3348 (instance_type, instance_id) 3349 @param affiliate: the affiliated PE, either as pe_id or as tuple 3350 (instance_type, instance_id) 3351 @param role: name of the role to remove the affiliate from, if None, 3352 the affiliate will be removed from all roles 3353 """ 3354 3355 db = current.db 3356 s3db = current.s3db 3357 3358 master_pe = pr_get_pe_id(master) 3359 affiliate_pe = pr_get_pe_id(affiliate) 3360 3361 if affiliate_pe: 3362 atable = s3db.pr_affiliation 3363 rtable = s3db.pr_role 3364 query = (atable.pe_id == affiliate_pe) & \ 3365 (atable.role_id == rtable.id) 3366 if master_pe: 3367 query &= (rtable.pe_id == master_pe) 3368 if role: 3369 query &= (rtable.role == role) 3370 rows = db(query).select(rtable.id) 3371 for row in rows: 3372 pr_remove_from_role(row.id, affiliate_pe) 3373 return
3374
3375 # ============================================================================= 3376 # PE Helpers 3377 # ============================================================================= 3378 # 3379 -def pr_get_pe_id(entity, record_id=None):
3380 """ 3381 Get the PE-ID of an instance record 3382 3383 @param entity: the entity, either a tablename, a tuple (tablename, 3384 record_id), a Row of the instance type, or a PE-ID 3385 @param record_id: the record ID, if entity is a tablename 3386 3387 @returns: the PE-ID 3388 """ 3389 3390 db = current.db 3391 s3db = current.s3db 3392 if record_id is not None: 3393 table, _id = entity, record_id 3394 elif isinstance(entity, (tuple, list)): 3395 table, _id = entity 3396 elif isinstance(entity, Row): 3397 if "pe_id" in entity: 3398 return entity["pe_id"] 3399 else: 3400 for f in entity.values(): 3401 if isinstance(f, Row) and "pe_id" in f: 3402 return f["pe_id"] 3403 return None 3404 elif isinstance(entity, (long, int)) or \ 3405 isinstance(entity, basestring) and entity.isdigit(): 3406 return entity 3407 else: 3408 return None 3409 if not hasattr(table, "_tablename"): 3410 table = s3db.table(table, None) 3411 record = None 3412 if table: 3413 if "pe_id" in table.fields and _id: 3414 record = db(table._id==_id).select(table.pe_id, 3415 limitby=(0, 1)).first() 3416 elif _id: 3417 key = table._id.name 3418 if key == "pe_id": 3419 return _id 3420 if key != "id" and "instance_type" in table.fields: 3421 s = db(table._id==_id).select(table.instance_type, 3422 limitby=(0, 1)).first() 3423 else: 3424 return None 3425 if not s: 3426 return None 3427 table = s3db.table(s.instance_type, None) 3428 if table and "pe_id" in table.fields: 3429 record = db(table[key] == _id).select(table.pe_id, 3430 limitby=(0, 1)).first() 3431 else: 3432 return None 3433 if record: 3434 return record.pe_id 3435 return None
3436
3437 # ============================================================================= 3438 # Back-end Role Tools 3439 # ============================================================================= 3440 # 3441 -def pr_define_role(pe_id, 3442 role=None, 3443 role_type=None, 3444 entity_type=None, 3445 sub_type=None):
3446 """ 3447 Back-end method to define a new affiliates-role for a person entity 3448 3449 @param pe_id: the person entity ID 3450 @param role: the role name 3451 @param role_type: the role type (from pr_role_types), default 9 3452 @param entity_type: limit selection in CRUD forms to this entity type 3453 @param sub_type: limit selection in CRUD forms to this entity sub-type 3454 3455 @returns: the role ID 3456 """ 3457 3458 db = current.db 3459 s3db = current.s3db 3460 3461 if not pe_id: 3462 return 3463 if role_type not in s3db.pr_role_types: 3464 role_type = 9 # Other 3465 3466 data = {"pe_id": pe_id, 3467 "role": role, 3468 "role_type": role_type, 3469 "entity_type": entity_type, 3470 "sub_type": sub_type} 3471 3472 rtable = s3db.pr_role 3473 if role: 3474 query = (rtable.pe_id == pe_id) & \ 3475 (rtable.role == role) 3476 duplicate = db(query).select(rtable.id, 3477 rtable.role_type, 3478 limitby=(0, 1)).first() 3479 else: 3480 duplicate = None 3481 if duplicate: 3482 if duplicate.role_type != role_type: 3483 # Clear paths if this changes the role type 3484 if str(role_type) != str(OU): 3485 data["path"] = None 3486 s3db.pr_role_rebuild_path(duplicate.id, clear=True) 3487 duplicate.update_record(**data) 3488 record_id = duplicate.id 3489 else: 3490 record_id = rtable.insert(**data) 3491 return record_id
3492
3493 # ============================================================================= 3494 -def pr_delete_role(role_id):
3495 """ 3496 Back-end method to delete a role 3497 3498 @param role_id: the role ID 3499 """ 3500 3501 manager = current.manager 3502 resource = manager.define_resource("pr", "role", id=role_id) 3503 return resource.delete()
3504
3505 # ============================================================================= 3506 -def pr_add_to_role(role_id, pe_id):
3507 """ 3508 Back-end method to add a person entity to a role. 3509 3510 @param role_id: the role ID 3511 @param pe_id: the person entity ID 3512 3513 @todo: update descendant paths only if the role is a OU role 3514 """ 3515 3516 db = current.db 3517 s3db = current.s3db 3518 3519 atable = s3db.pr_affiliation 3520 3521 # Check for duplicate 3522 query = (atable.role_id == role_id) & (atable.pe_id == pe_id) 3523 affiliation = db(query).select(limitby=(0, 1)).first() 3524 if affiliation is None: 3525 # Insert affiliation record 3526 atable.insert(role_id=role_id, pe_id=pe_id) 3527 # Clear descendant paths (triggers lazy rebuild) 3528 pr_rebuild_path(pe_id, clear=True) 3529 return
3530
3531 # ============================================================================= 3532 -def pr_remove_from_role(role_id, pe_id):
3533 """ 3534 Back-end method to remove a person entity from a role. 3535 3536 @param role_id: the role ID 3537 @param pe_id: the person entity ID 3538 3539 @todo: update descendant paths only if the role is a OU role 3540 """ 3541 3542 db = current.db 3543 s3db = current.s3db 3544 3545 atable = s3db.pr_affiliation 3546 3547 query = (atable.role_id == role_id) & (atable.pe_id == pe_id) 3548 affiliation = db(query).select(limitby=(0, 1)).first() 3549 if affiliation is not None: 3550 # Soft-delete the record, clear foreign keys 3551 deleted_fk = {"role_id": role_id, "pe_id": pe_id} 3552 data = {"deleted": True, 3553 "role_id": None, 3554 "pe_id": None, 3555 "deleted_fk": json.dumps(deleted_fk)} 3556 affiliation.update_record(**data) 3557 # Clear descendant paths 3558 pr_rebuild_path(pe_id, clear=True) 3559 return
3560
3561 # ============================================================================= 3562 # Hierarchy Lookup 3563 # ============================================================================= 3564 # 3565 -def pr_get_role_paths(pe_id, roles=None, role_types=None):
3566 """ 3567 Get the ancestor paths of the ancestor OUs this person entity 3568 is affiliated with, sorted by roles. 3569 3570 Used by gis.set_config() 3571 3572 @param pe_id: the person entity ID 3573 @param roles: list of roles to limit the search 3574 @param role_types: list of role types to limit the search 3575 3576 @returns: a Storage() of S3MultiPaths with the role names as keys 3577 3578 @note: role_types is ignored if roles gets specified 3579 """ 3580 3581 db = current.db 3582 s3db = current.s3db 3583 3584 atable = s3db.pr_affiliation 3585 rtable = s3db.pr_role 3586 3587 query = (atable.deleted != True) & \ 3588 (atable.role_id == rtable.id) & \ 3589 (atable.pe_id == pe_id) & \ 3590 (rtable.deleted != True) 3591 3592 if roles is not None: 3593 # Limit the lookup to these roles 3594 if not isinstance(roles, (list, tuple)): 3595 roles = [roles] 3596 query &= (rtable.role.belongs(roles)) 3597 elif role_types is not None: 3598 # Limit the lookup to these types of roles 3599 if not isinstance(role_types, (list, tuple)): 3600 role_types = [role_types] 3601 query &= (rtable.role_type.belongs(role_types)) 3602 3603 rows = db(query).select(rtable.role, rtable.path, rtable.pe_id) 3604 3605 role_paths = Storage() 3606 for role in rows: 3607 name = role.role 3608 if name in role_paths: 3609 multipath = role_paths[name] 3610 multipath.append([role.pe_id]) 3611 else: 3612 multipath = S3MultiPath([role.pe_id]) 3613 path = pr_get_path(role.pe_id) 3614 multipath.extend(role.pe_id, path, cut=pe_id) 3615 role_paths[name] = multipath.clean() 3616 3617 return role_paths
3618
3619 # ============================================================================= 3620 -def pr_get_role_branches(pe_id, 3621 roles=None, 3622 role_types=None, 3623 entity_type=None):
3624 """ 3625 Get all descendants of the immediate ancestors of the entity 3626 within these roles/role types 3627 3628 @param pe_id: the person entity ID 3629 @param roles: list of roles to limit the search 3630 @param role_types: list of role types to limit the search 3631 @param entity_type: limit the result to this entity type 3632 3633 @returns: a list of PE-IDs 3634 3635 @note: role_types is ignored if roles gets specified 3636 """ 3637 3638 db = current.db 3639 s3db = current.s3db 3640 3641 rtable = s3db.pr_role 3642 atable = s3db.pr_affiliation 3643 etable = s3db.pr_pentity 3644 3645 query = (atable.deleted != True) & \ 3646 (atable.pe_id == pe_id) & \ 3647 (atable.role_id == rtable.id) & \ 3648 (rtable.pe_id == etable.pe_id) 3649 3650 if roles is not None: 3651 # Limit the search to these roles 3652 if not isinstance(roles, (list, tuple)): 3653 roles = [roles] 3654 query &= (rtable.role.belongs(roles)) 3655 3656 elif role_types is not None: 3657 # Limit the search to these types of roles 3658 if not isinstance(role_types, (list, tuple)): 3659 role_types = [role_types] 3660 query &= (rtable.role_type.belongs(role_types)) 3661 3662 rows = db(query).select(rtable.pe_id, etable.instance_type) 3663 3664 # Table names to retrieve the data from the rows 3665 rtn = rtable._tablename 3666 etn = etable._tablename 3667 3668 # Get the immediate ancestors 3669 nodes = [r[rtn].pe_id for r in rows] 3670 3671 # Filter the result by entity type 3672 result = [r[rtn].pe_id for r in rows 3673 if entity_type is None or r[etn].instance_type == entity_type] 3674 3675 # Get the branches 3676 branches = pr_get_descendants(nodes, entity_type=entity_type) 3677 3678 return result + branches
3679
3680 # ============================================================================= 3681 -def pr_get_path(pe_id):
3682 """ 3683 Get all ancestor paths of a person entity 3684 3685 @param pe_id: the person entity ID 3686 3687 @returns: an S3MultiPath instance 3688 """ 3689 3690 db = current.db 3691 s3db = current.s3db 3692 3693 atable = s3db.pr_affiliation 3694 rtable = s3db.pr_role 3695 3696 query = (atable.deleted != True) & \ 3697 (atable.role_id == rtable.id) & \ 3698 (atable.pe_id == pe_id) & \ 3699 (rtable.deleted != True) & \ 3700 (rtable.role_type == OU) 3701 roles = db(query).select(rtable.ALL) 3702 multipath = S3MultiPath() 3703 append = multipath.append 3704 for role in roles: 3705 path = S3MultiPath([role.pe_id]) 3706 if role.path is None: 3707 ppath = pr_role_rebuild_path(role) 3708 else: 3709 ppath = S3MultiPath(role.path) 3710 path.extend(role.pe_id, ppath, cut=pe_id) 3711 for p in path.paths: 3712 append(p) 3713 return multipath.clean()
3714
3715 # ============================================================================= 3716 -def pr_get_ancestors(pe_id):
3717 """ 3718 Find all ancestor entities of a person entity in the OU hierarchy 3719 (performs a path lookup where paths are available, otherwise rebuilds 3720 paths). 3721 3722 @param pe_id: the person entity ID 3723 @param roles: list of roles to limit the search 3724 3725 @returns: a list of PE-IDs 3726 """ 3727 3728 db = current.db 3729 s3db = current.s3db 3730 3731 atable = s3db.pr_affiliation 3732 rtable = s3db.pr_role 3733 3734 query = (atable.deleted != True) & \ 3735 (atable.role_id == rtable.id) & \ 3736 (atable.pe_id == pe_id) & \ 3737 (rtable.deleted != True) & \ 3738 (rtable.role_type == OU) 3739 3740 roles = db(query).select(rtable.ALL) 3741 3742 paths = [] 3743 append = paths.append 3744 for role in roles: 3745 path = S3MultiPath([role.pe_id]) 3746 if role.path is None: 3747 ppath = pr_role_rebuild_path(role) 3748 else: 3749 ppath = S3MultiPath(role.path) 3750 path.extend(role.pe_id, ppath, cut=pe_id) 3751 append(path) 3752 ancestors = S3MultiPath.all_nodes(paths) 3753 3754 return ancestors
3755
3756 # ============================================================================= 3757 -def pr_realm(entity):
3758 """ 3759 Get the default realm (=the immediate OU ancestors) of an entity 3760 3761 @param entity: the entity (pe_id) 3762 """ 3763 3764 db = current.db 3765 s3db = current.s3db 3766 3767 atable = s3db.pr_affiliation 3768 rtable = s3db.pr_role 3769 3770 if not entity: 3771 return [] 3772 query = (atable.deleted != True) & \ 3773 (atable.role_id == rtable.id) & \ 3774 (atable.pe_id == entity) & \ 3775 (rtable.deleted != True) & \ 3776 (rtable.role_type == OU) 3777 rows = db(query).select(rtable.pe_id) 3778 realm = [row.pe_id for row in rows] 3779 return realm
3780
3781 # ============================================================================= 3782 -def pr_ancestors(entities):
3783 """ 3784 Find all ancestor entities of the given entities in the 3785 OU hierarchy. 3786 3787 @param pe_id: the person entity ID 3788 @param roles: list of roles to limit the search 3789 3790 @returns: Storage of lists of PE-IDs 3791 """ 3792 3793 db = current.db 3794 s3db = current.s3db 3795 3796 atable = s3db.pr_affiliation 3797 rtable = s3db.pr_role 3798 3799 if not entities: 3800 return Storage() 3801 3802 query = (atable.deleted != True) & \ 3803 (atable.role_id == rtable.id) & \ 3804 (atable.pe_id.belongs(entities)) & \ 3805 (rtable.deleted != True) & \ 3806 (rtable.role_type == OU) 3807 rows = db(query).select(rtable.ALL, atable.pe_id) 3808 3809 ancestors = Storage([(pe_id, []) for pe_id in entities]) 3810 r = rtable._tablename 3811 a = atable._tablename 3812 for row in rows: 3813 pe_id = row[a].pe_id 3814 paths = ancestors[pe_id] 3815 role = row[r] 3816 path = S3MultiPath([role.pe_id]) 3817 if role.path is None: 3818 ppath = pr_role_rebuild_path(role) 3819 else: 3820 ppath = S3MultiPath(role.path) 3821 path.extend(role.pe_id, ppath, cut=pe_id) 3822 paths.append(path) 3823 for pe_id in ancestors: 3824 ancestors[pe_id] = S3MultiPath.all_nodes(ancestors[pe_id]) 3825 return ancestors
3826
3827 # ============================================================================= 3828 -def pr_descendants(pe_ids, skip=[]):
3829 3830 db = current.db 3831 s3db = current.s3db 3832 3833 pe_ids = [i for i in pe_ids if i not in skip] 3834 if not pe_ids: 3835 return [] 3836 3837 etable = s3db.pr_pentity 3838 rtable = s3db.pr_role 3839 atable = s3db.pr_affiliation 3840 3841 query = (rtable.deleted != True) & \ 3842 (rtable.pe_id.belongs(pe_ids)) & \ 3843 (rtable.role_type == OU) & \ 3844 (atable.role_id == rtable.id) & \ 3845 (atable.deleted != True) & \ 3846 (etable.pe_id == atable.pe_id) 3847 3848 rows = db(query).select(rtable.pe_id, 3849 atable.pe_id, 3850 etable.instance_type) 3851 r = rtable._tablename 3852 e = etable._tablename 3853 a = atable._tablename 3854 3855 nodes = [] 3856 result = Storage() 3857 for row in rows: 3858 parent = row[r].pe_id 3859 child = row[a].pe_id 3860 if row[e].instance_type != "pr_person": 3861 if parent not in result: 3862 result[parent] = [child] 3863 else: 3864 result[parent].append(child) 3865 if child not in nodes: 3866 nodes.append(child) 3867 3868 skip = skip + pe_ids 3869 descendants = pr_descendants(nodes, skip=skip) 3870 3871 for child in descendants: 3872 for parent in result: 3873 if child in result[parent]: 3874 for node in descendants[child]: 3875 if node not in result[parent]: 3876 result[parent].append(node) 3877 for x in result: 3878 for y in result: 3879 if x in result[y]: 3880 result[y].extend([i for i in result[x] if i not in result[y] and i != y]) 3881 3882 return result
3883
3884 # ============================================================================= 3885 -def pr_get_descendants(pe_ids, skip=[], entity_type=None, ids=True):
3886 """ 3887 Find descendant entities of a person entity in the OU hierarchy 3888 (performs a real search, not a path lookup). 3889 3890 @param pe_ids: person entity ID or list of IDs 3891 @param skip: list of person entity IDs to skip during descending 3892 3893 @returns: a list of PE-IDs 3894 """ 3895 3896 db = current.db 3897 s3db = current.s3db 3898 3899 etable = s3db.pr_pentity 3900 rtable = s3db.pr_role 3901 atable = s3db.pr_affiliation 3902 3903 en = etable._tablename 3904 an = atable._tablename 3905 3906 if type(pe_ids) is not list: 3907 pe_ids = [pe_ids] 3908 pe_ids = [i for i in pe_ids if i not in skip] 3909 if not pe_ids: 3910 return [] 3911 query = (rtable.deleted != True) & \ 3912 (rtable.pe_id.belongs(pe_ids)) & \ 3913 (~(rtable.pe_id.belongs(skip))) &\ 3914 (rtable.role_type == OU) & \ 3915 (atable.deleted != True) & \ 3916 (atable.role_id == rtable.id) & \ 3917 (etable.pe_id == atable.pe_id) 3918 skip = skip + pe_ids 3919 rows = db(query).select(atable.pe_id, 3920 etable.instance_type) 3921 nodes = [(r[an].pe_id, r[en].instance_type) for r in rows] 3922 result = [] 3923 append = result.append 3924 for n in nodes: 3925 if n not in result: 3926 append(n) 3927 node_ids = [n[0] for n in result] 3928 descendants = pr_get_descendants(node_ids, skip=skip, ids=False) 3929 for d in descendants: 3930 if d not in result: 3931 append(d) 3932 3933 if ids: 3934 return [n[0] 3935 for n in result 3936 if entity_type is None or n[1] == entity_type] 3937 else: 3938 return result
3939
3940 # ============================================================================= 3941 # Internal Path Tools 3942 # ============================================================================= 3943 # 3944 -def pr_rebuild_path(pe_id, clear=False):
3945 """ 3946 Rebuild the ancestor path of all roles in the OU hierarchy a person 3947 entity defines. 3948 3949 @param pe_id: the person entity ID 3950 @param clear: clear paths in descendant roles (triggers lazy rebuild) 3951 """ 3952 3953 db = current.db 3954 s3db = current.s3db 3955 3956 if isinstance(pe_id, Row): 3957 pe_id = row.pe_id 3958 3959 rtable = s3db.pr_role 3960 query = (rtable.deleted != True) & \ 3961 (rtable.pe_id == pe_id) & \ 3962 (rtable.role_type == OU) 3963 db(query).update(path=None) 3964 roles = db(query).select() 3965 for role in roles: 3966 if role.path is None: 3967 pr_role_rebuild_path(role, clear=clear) 3968 return
3969
3970 # ============================================================================= 3971 -def pr_role_rebuild_path(role_id, skip=[], clear=False):
3972 """ 3973 Rebuild the ancestor path of a role within the OU hierarchy 3974 3975 @param role_id: the role ID 3976 @param skip: list of role IDs to skip during recursion 3977 @param clear: clear paths in descendant roles (triggers lazy rebuild) 3978 """ 3979 3980 db = current.db 3981 s3db = current.s3db 3982 3983 rtable = s3db.pr_role 3984 atable = s3db.pr_affiliation 3985 3986 if isinstance(role_id, Row): 3987 role = role_id 3988 role_id = role.id 3989 else: 3990 query = (rtable.deleted != True) & \ 3991 (rtable.id == role_id) 3992 role = db(query).select(limitby=(0, 1)).first() 3993 if not role: 3994 return None 3995 pe_id = role.pe_id 3996 3997 if role_id in skip: 3998 return role.path 3999 skip = skip + [role_id] 4000 4001 if role.role_type != OU: 4002 path = None 4003 else: 4004 # Get all parent roles 4005 query = (atable.deleted != True) & \ 4006 (atable.pe_id == pe_id) & \ 4007 (rtable.deleted != True) & \ 4008 (rtable.id == atable.role_id) & \ 4009 (rtable.role_type == OU) 4010 parent_roles = db(query).select(rtable.ALL) 4011 4012 # Update ancestor path 4013 path = S3MultiPath() 4014 for prole in parent_roles: 4015 path.append([prole.pe_id]) 4016 if prole.path is None: 4017 ppath = pr_role_rebuild_path(prole, skip=skip) 4018 else: 4019 ppath = S3MultiPath(prole.path) 4020 if ppath is not None: 4021 path.extend(prole.pe_id, ppath, cut=pe_id) 4022 db(rtable.id == role_id).update(path=str(path)) 4023 4024 # Clear descendant paths, if requested (only necessary for writes) 4025 if clear: 4026 query = (rtable.deleted != True) & \ 4027 (rtable.path.like("%%|%s|%%" % pe_id)) & \ 4028 (~(rtable.id.belongs(skip))) 4029 db(query).update(path=None) 4030 4031 return path
4032 4033 # END ========================================================================= 4034