├── .gitignore ├── .idea ├── crowdsource-reporter-scripts.iml ├── misc.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── CityworksConnection ├── Connect2Cityworks.pyt └── connect_to_cityworks.py ├── LICENSE.txt ├── README.md ├── ServiceSupport.Emails.pyt.xml ├── ServiceSupport.Enrich.pyt.xml ├── ServiceSupport.General.pyt.xml ├── ServiceSupport.Identifiers.pyt.xml ├── ServiceSupport.Moderate.pyt.xml ├── ServiceSupport.pyt ├── ServiceSupport.pyt.xml ├── WorkforceConnection ├── New Python Toolbox.Tool.pyt.xml ├── New Python Toolbox.Workforce.pyt.xml ├── New Python Toolbox.pyt.xml ├── Workforce Connection.pyt ├── Workforce Connection.pyt.xml └── create_workforce_assignments.py ├── internal_email_template.html ├── send_email.py ├── servicefunctions.py └── user_email_template.html /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ############# 3 | ## Python 4 | ############# 5 | 6 | *.py[co] 7 | *.pyc 8 | 9 | 10 | # Tools, notes, outputs 11 | 12 | *.log 13 | 14 | -------------------------------------------------------------------------------- /.idea/crowdsource-reporter-scripts.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 118 | 119 | 125 | 126 | 127 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 156 | 157 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 187 | 188 | 204 | 205 | 216 | 217 | 235 | 236 | 254 | 255 | 275 | 276 | 297 | 298 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 333 | 334 | 335 | 336 | 1517336566252 337 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 369 | 370 | 371 | 372 | 373 | file://$PROJECT_DIR$/servicefunctions.py 374 | 241 375 | 377 | 378 | 379 | 380 | 381 | 383 | 384 | 385 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | -------------------------------------------------------------------------------- /CityworksConnection/Connect2Cityworks.pyt: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: connect_to_cityworks.pyt 3 | # Purpose: Pass reports from esri to cityworks 4 | # 5 | # Author: alli6394 6 | # 7 | # Created: 31/10/2016 8 | # 9 | # Version: Unreleased 10 | 11 | # Copyright 2016 Esri 12 | 13 | # Licensed under the Apache License, Version 2.0 (the "License"); 14 | # you may not use this file except in compliance with the License. 15 | # You may obtain a copy of the License at 16 | # 17 | # http://www.apache.org/licenses/LICENSE-2.0 18 | # 19 | # Unless required by applicable law or agreed to in writing, software 20 | # distributed under the License is distributed on an "AS IS" BASIS, 21 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | # See the License for the specific language governing permissions and 23 | # limitations under the License. 24 | 25 | # ------------------------------------------------------------------------------ 26 | 27 | import arcpy 28 | from arcgis.gis import GIS, Group, Layer 29 | from arcgis.mapping import WebMap 30 | from arcgis.features import FeatureLayer, Table 31 | 32 | import json 33 | from pytz import common_timezones 34 | 35 | timezones = common_timezones 36 | cityworksfields = ['AcctNum', 'Address', 'Answers', 'AptNum', 'CallerAcctNum', 'CallerAddress', 'CallerAptNum', 'CallerCallTime', 'CallerCellPhone', 'CallerCity', 'CallerComments', 'CallerDistrict', 'CallerEmail', 'CallerFax', 'CallerFirstName', 'CallerHomePhone', 'CallerIsFollowUpCall', 'CallerIsOwner', 'CallerLastName', 'CallerMiddleInitial', 'CallerOtherPhone', 'CallerState', 'CallerText1', 'CallerText2', 'CallerText3', 'CallerText4', 'CallerText5', 'CallerTitle', 'CallerType', 'CallerWorkPhone', 'CallerZip', 'Cancel', 'CancelReason', 'CancelledBy', 'CancelledBySid', 'Ccx', 'Ccy', 'CellPhone', 'City', 'ClosedBy', 'ClosedBySid', 'Comments', 'CustAddType', 'CustAddress', 'CustCallback', 'CustCity', 'CustContact', 'CustDistrict', 'CustState', 'CustZip', 'CustomFieldValues', 'Date1', 'Date2', 'Date3', 'Date4', 'Date5', 'DateCancelled', 'DateDispatchOpen', 'DateDispatchTo', 'DateInvtDone', 'DateSubmitTo', 'DateSubmitToOpen', 'DateTimeCall', 'DateTimeCallback', 'DateTimeClosed', 'DateTimeContact', 'DateTimeInit', 'Description', 'Details', 'DispatchOpenBy', 'DispatchOpenBySid', 'DispatchTo', 'DispatchToSid', 'DispatchToUseDispatchToSid', 'District', 'DomainId', 'Effort', 'Email', 'EmployeeSid', 'Excursion', 'Fax', 'FieldInvtDone', 'FirstName', 'HomePhone', 'InitiatedBy', 'InitiatedByApp', 'InitiatedBySid', 'IsClosed', 'IsFollowUpCall', 'IsResident', 'LaborCost', 'Landmark', 'LastName', 'Location', 'LockedByDesktopUser', 'MapPage', 'MiddleInitial', 'Num1', 'Num2', 'Num3', 'Num4', 'Num5', 'OtherPhone', 'OtherSystemCode', 'OtherSystemDesc', 'OtherSystemDesc2', 'OtherSystemId', 'OtherSystemStatus', 'Priority', 'PrjCompleteDate', 'ProbAddType', 'ProbAddress', 'ProbAptNum', 'ProbCity', 'ProbDetails', 'ProbDistrict', 'ProbLandmark', 'ProbLocation', 'ProbState', 'ProbZip', 'ProblemCode', 'ProblemSid', 'ProjectName', 'ProjectSid', 'ReqCategory', 'ReqCustFieldCatId', 'RequestId', 'Resolution', 'SRX', 'SRY', 'Shop', 'State', 'Status', 'StreetName', 'SubmitTo', 'SubmitToEmail', 'SubmitToOpenBy', 'SubmitToOpenBySid', 'SubmitToPager', 'SubmitToPhone', 'SubmitToSid', 'SubmitToUseSubmitToSid', 'Text1', 'Text10', 'Text11', 'Text12', 'Text13', 'Text14', 'Text15', 'Text16', 'Text17', 'Text18', 'Text19', 'Text2', 'Text20', 'Text3', 'Text4', 'Text5', 'Text6', 'Text7', 'Text8', 'Text9', 'TileNo', 'Title', 'WONeeded', 'WorkOrderId', 'WorkPhone', 'X', 'Y', 'Zip'] 37 | 38 | layer_fields = [] 39 | table_fields = [] 40 | groupid = '' 41 | agol_user = '' 42 | agol_pass = '' 43 | agol_org = '' 44 | layer_list = '' 45 | table_list = '' 46 | gis = '' 47 | flag_field = '' 48 | 49 | 50 | class Toolbox(object): 51 | def __init__(self): 52 | """Define the toolbox (the name of the toolbox is the name of the 53 | .pyt file).""" 54 | self.label = "Toolbox" 55 | self.alias = "" 56 | 57 | # List of tool classes associated with this toolbox 58 | self.tools = [Tool] 59 | 60 | 61 | class Tool(object): 62 | def __init__(self): 63 | """Define the tool (tool name is the name of the class).""" 64 | self.label = "Tool" 65 | self.description = "" 66 | self.canRunInBackground = False 67 | 68 | def getParameterInfo(self): 69 | """Define parameter definitions""" 70 | cw_url = arcpy.Parameter( 71 | displayName="Cityworks URL", 72 | name="cityworks_url", 73 | datatype="GPString", 74 | parameterType="Required", 75 | direction="Input" 76 | ) 77 | cw_user = arcpy.Parameter( 78 | displayName="Cityworks Username", 79 | name="cityworks_user", 80 | datatype="GPString", 81 | parameterType="Required", 82 | direction="Input" 83 | ) 84 | cw_pw = arcpy.Parameter( 85 | displayName="Cityworks Password", 86 | name="cityworks_password", 87 | datatype="GPString", 88 | parameterType="Required", 89 | direction="Input" 90 | ) 91 | cw_timezone = arcpy.Parameter( 92 | displayName="Timezone of the Cityworks server", 93 | name="cw_timezone", 94 | datatype="GPString", 95 | parameterType="Required", 96 | direction="Input" 97 | ) 98 | cw_cwol = arcpy.Parameter( 99 | displayName="Cityworks Online Site?", 100 | name="cityworks_cwol", 101 | datatype="GPBoolean", 102 | parameterType="Required", 103 | direction="Input" 104 | ) 105 | portal_url = arcpy.Parameter( 106 | displayName="ArcGIS URL", 107 | name="portal_url", 108 | datatype="GPString", 109 | parameterType="Required", 110 | direction="Input" 111 | ) 112 | 113 | portal_user = arcpy.Parameter( 114 | displayName="ArcGIS Username", 115 | name="portal_user", 116 | datatype="GPString", 117 | parameterType="Required", 118 | direction="Input" 119 | ) 120 | 121 | portal_pw = arcpy.Parameter( 122 | displayName="ArcGIS Password", 123 | name="portal_password", 124 | datatype="GPString", 125 | parameterType="Required", 126 | direction="Input" 127 | ) 128 | group = arcpy.Parameter( 129 | displayName="Reporter Group", 130 | name="group", 131 | datatype="GPString", 132 | parameterType="Required", 133 | direction="Input" 134 | ) 135 | flayers = arcpy.Parameter( 136 | displayName="Layers", 137 | name="layers", 138 | datatype="GPString", 139 | parameterType="Required", 140 | direction="Input", 141 | multiValue=True 142 | ) 143 | ftables = arcpy.Parameter( 144 | displayName="Tables", 145 | name="tables", 146 | datatype="GPString", 147 | parameterType="Optional", 148 | direction="Input", 149 | multiValue=True 150 | ) 151 | cw_id = arcpy.Parameter( 152 | displayName="Cityworks Report ID Field", 153 | name="cw_id", 154 | datatype="GPString", 155 | parameterType="Required", 156 | direction="Input" 157 | ) 158 | report_id = arcpy.Parameter( 159 | displayName="ArcGIS Report ID Field", 160 | name="report_id", 161 | datatype="GPString", 162 | parameterType="Required", 163 | direction="Input" 164 | ) 165 | cw_probtype = arcpy.Parameter( 166 | displayName="Cityworks Report Type Field", 167 | name="cw_probtype", 168 | datatype="GPString", 169 | parameterType="Required", 170 | direction="Input" 171 | ) 172 | report_type = arcpy.Parameter( 173 | displayName="ArcGIS Report Type Field", 174 | name="report_type", 175 | datatype="GPString", 176 | parameterType="Required", 177 | direction="Input" 178 | ) 179 | cw_opendate = arcpy.Parameter( 180 | displayName="Cityworks Open Date Field", 181 | name="cw_opendate", 182 | datatype="GPString", 183 | parameterType="Required", 184 | direction="Input" 185 | ) 186 | report_opendate = arcpy.Parameter( 187 | displayName="ArcGIS Open Date Field", 188 | name="report_opendate", 189 | datatype="GPString", 190 | parameterType="Required", 191 | direction="Input" 192 | ) 193 | fl_flds = arcpy.Parameter( 194 | displayName="Report Layer Field Map", 195 | name="feature_fields", 196 | datatype="GPValueTable", 197 | parameterType="Required", 198 | direction="Input" 199 | ) 200 | tb_flds = arcpy.Parameter( 201 | displayName="Comment Table Field Map", 202 | name="comment_fields", 203 | datatype="GPValueTable", 204 | parameterType="Optional", 205 | direction="Input" 206 | ) 207 | flag_fld = arcpy.Parameter( 208 | displayName="Flag Field", 209 | name="flag_field", 210 | datatype="GPString", 211 | parameterType="Required", 212 | direction="Input" 213 | ) 214 | flag_on = arcpy.Parameter( 215 | displayName="Flag On Value", 216 | name="flag_on", 217 | datatype="GPString", 218 | parameterType="Required", 219 | direction="Input" 220 | ) 221 | flag_off = arcpy.Parameter( 222 | displayName="Flag Off Value", 223 | name="flag_off", 224 | datatype="GPString", 225 | parameterType="Required", 226 | direction="Input" 227 | ) 228 | config_path = arcpy.Parameter( 229 | displayName="Save a Configuration File", 230 | name="configuration_path", 231 | datatype="DEFile", 232 | parameterType="Optional", 233 | direction="Output" 234 | ) 235 | 236 | cw_cwol.value = False 237 | 238 | group.filter.type = 'ValueList' 239 | group.filter.list = ['Provide ArcGIS credentials to see group list'] 240 | 241 | fl_flds.columns = [['GPString', 'ArcGIS Field'], ['GPString', 'Cityworks Field']] 242 | fl_flds.filters[1].type = 'ValueList' 243 | fl_flds.filters[1].list = cityworksfields 244 | fl_flds.filters[0].type = 'ValueList' 245 | fl_flds.filters[0].list = ['Provide credentials and select a group to see field list'] 246 | 247 | flag_fld.filter.type = 'ValueList' 248 | flag_fld.filter.list = ['Provide credentials and select a group to see field list'] 249 | 250 | tb_flds.columns = [['GPString', 'ArcGIS Field'], ['GPString', 'Cityworks Field']] 251 | tb_flds.filters[1].type = 'ValueList' 252 | tb_flds.filters[1].list = cityworksfields 253 | tb_flds.filters[0].type = 'ValueList' 254 | tb_flds.filters[0].list = ['1', '2'] 255 | 256 | flayers.filter.type = 'ValueList' 257 | flayers.filter.list = ['Provide credentials and select a group to see field list'] 258 | 259 | ftables.filter.type = 'ValueList' 260 | ftables.filter.list = ['Provide credentials and select a group to see table list'] 261 | 262 | cw_id.filter.type = 'ValueList' 263 | cw_id.filter.list = cityworksfields 264 | 265 | report_id.filter.type = 'ValueList' 266 | report_id.filter.list = ['Select layers to see field list'] 267 | 268 | cw_probtype.filter.type = 'ValueList' 269 | cw_probtype.filter.list = cityworksfields 270 | 271 | report_type.filter.type = 'ValueList' 272 | report_type.filter.list = ['Select layers to see field list'] 273 | 274 | cw_opendate.filter.type = 'ValueList' 275 | cw_opendate.filter.list = cityworksfields 276 | 277 | report_opendate.filter.type = 'ValueList' 278 | report_opendate.filter.list = ['Select layers to see field list'] 279 | 280 | cw_timezone.filter.type = 'ValueList' 281 | cw_timezone.filter.list = timezones 282 | 283 | params = [portal_url, portal_user, portal_pw, cw_url, cw_user, cw_pw, cw_timezone, cw_cwol, group, flayers, cw_id, report_id, 284 | cw_probtype, report_type, cw_opendate, report_opendate, flag_fld, flag_on, flag_off, fl_flds, ftables, tb_flds, config_path] 285 | 286 | return params 287 | 288 | def isLicensed(self): 289 | """Set whether tool is licensed to execute.""" 290 | return True 291 | 292 | def updateParameters(self, parameters): 293 | """Modify the values and properties of parameters before internal 294 | validation is performed. This method is called whenever a parameter 295 | has been changed.""" 296 | 297 | portal_url, portal_user, portal_pw, cw_url, cw_user, cw_pw, cw_timezone, cw_cwol, group, flayers, cw_id, report_id, cw_probtype, report_type, cw_opendate, report_opendate, flag_fld, flag_on, flag_off, fl_flds, ftables, tb_flds, config_path = parameters 298 | 299 | global agol_user 300 | global agol_pass 301 | global agol_org 302 | global groupid 303 | global layer_list 304 | global table_list 305 | global layer_fields 306 | global table_fields 307 | global gis 308 | global flag_field 309 | 310 | # Get list of groups available to the user 311 | if not portal_url.value or not portal_pw.value or not portal_user.value: # or not cw_url.value or not cw_pw.value or not cw_user.value: 312 | group.value = '' 313 | group.enabled = False 314 | agol_org = '' 315 | agol_pass = '' 316 | agol_user = '' 317 | 318 | elif portal_url.valueAsText != agol_org or portal_pw.valueAsText != agol_pass or portal_user.valueAsText != agol_user: 319 | group.enabled = True 320 | 321 | agol_org = portal_url.valueAsText 322 | agol_pass = portal_pw.valueAsText 323 | agol_user = portal_user.valueAsText 324 | 325 | gis = GIS(agol_org, agol_user, agol_pass) 326 | group.filter.list = ['{} ({})'.format(group.title, group.id) for group in gis.groups.search()] 327 | 328 | # Get list of layers in all maps shared with the group 329 | if not group.value: 330 | flayers.value = [] 331 | flayers.enabled = False 332 | ftables.value = [] 333 | ftables.enabled = False 334 | groupid = '' 335 | 336 | elif group.valueAsText != groupid: 337 | flayers.enabled = True 338 | ftables.enabled = True 339 | 340 | groupid = group.valueAsText 341 | layer_urls = [] 342 | table_urls = [] 343 | 344 | # Get group 345 | agolgroup = Group(gis, groupid.split(' ')[-1][1:-1]) 346 | 347 | # Get maps in group 348 | maps = [item for item in agolgroup.content() if item.type == 'Web Map'] 349 | 350 | for mapitem in maps: 351 | webmap = WebMap(mapitem) 352 | 353 | for layer in webmap.definition['operationalLayers']: 354 | lyr = FeatureLayer(layer['url'], gis) 355 | 356 | if 'Create' in lyr.properties.capabilities: # Reports layer must have 'create' capabilities 357 | try: 358 | for field in layer['popupInfo']['fieldInfos']: 359 | # reports layer must have at least one editable field 360 | if field['isEditable'] and 'relationships/' not in field['fieldName']: 361 | layer_urls.append('{} ({})'.format(lyr.properties.name, layer['url'])) 362 | break 363 | 364 | except KeyError: 365 | pass # if no popup, layer can't be reports layer 366 | 367 | try: 368 | for table in webmap.definition['tables']: 369 | tab = Table(table['url'], gis) 370 | 371 | for field in table['popupInfo']['fieldInfos']: 372 | if field['isEditable'] and 'relationships/' not in field['fieldName']: 373 | # comment table must have at least one editable field 374 | table_urls.append('{} ({})'.format(tab.properties.name, table['url'])) 375 | break 376 | 377 | except KeyError: 378 | pass # if no table/popup, no comments layer 379 | 380 | layer_urls = list(set(layer_urls)) 381 | if len(table_urls) > 0: 382 | table_urls = list(set(table_urls)) 383 | flayers.filter.list = layer_urls 384 | flayers.value = layer_urls 385 | ftables.filter.list = table_urls 386 | ftables.value = table_urls 387 | 388 | # If layers are changed 389 | if not flayers.value: 390 | layer_list = '' 391 | flag_field = '' 392 | cw_id.filter.list = [] 393 | cw_id.value = '' 394 | cw_id.enabled = False 395 | report_id.value = '' 396 | report_id.filter.list = [] 397 | report_id.enabled = False 398 | cw_probtype.value = '' 399 | cw_probtype.filter.list = [] 400 | cw_probtype.enabled = False 401 | report_type.value = '' 402 | report_type.filter.list = [] 403 | report_type.enabled = False 404 | cw_opendate.value = '' 405 | cw_opendate.filter.list = [] 406 | cw_opendate.enabled = False 407 | report_opendate.value = '' 408 | report_opendate.filter.list = [] 409 | report_opendate.enabled = False 410 | flag_fld.value = '' 411 | flag_fld.filter.list = [] 412 | flag_fld.enabled = False 413 | flag_on.value = '' 414 | flag_on.filter.list = [] 415 | flag_on.enabled = False 416 | flag_off.value = '' 417 | flag_off.filter.list = [] 418 | flag_off.enabled = False 419 | fl_flds.filters[0].list = ['Provide credentials and select a group to see field list'] 420 | fl_flds.value = '' 421 | fl_flds.enabled = False 422 | flag_fld.filter.list = table_fields 423 | 424 | elif flayers.valueAsText != layer_list: 425 | cw_id.enabled = True 426 | report_id.enabled = True 427 | cw_probtype.enabled = True 428 | report_type.enabled = True 429 | cw_opendate.enabled = True 430 | report_opendate.enabled = True 431 | flag_fld.enabled = True 432 | flag_on.enabled = True 433 | flag_off.enabled = True 434 | fl_flds.enabled = True 435 | 436 | layer_fields = [] 437 | layer_list = flayers.valueAsText 438 | 439 | # If layers are updated 440 | services = [item.split(' ')[-1][1:-2] for item in str(flayers.value).split(';')] 441 | 442 | for url in services: 443 | lyr = FeatureLayer(url, gis) 444 | new_fields = [field['name'] for field in lyr.properties.fields] 445 | 446 | if layer_fields: 447 | layer_fields = list(set(new_fields) & set(layer_fields)) 448 | else: 449 | layer_fields = new_fields 450 | 451 | fl_flds.filters[0].list = layer_fields 452 | report_id.filter.list = layer_fields 453 | report_type.filter.list = layer_fields 454 | report_opendate.filter.list = layer_fields 455 | 456 | if 'RequestId' in cityworksfields: 457 | cw_id.value = 'RequestId' 458 | if 'ProblemSid' in cityworksfields: 459 | cw_probtype.value = 'ProblemSid' 460 | if 'DateTimeInit' in cityworksfields: 461 | cw_opendate.value = 'DateTimeInit' 462 | 463 | if 'REPORTID' in layer_fields: 464 | report_id.value = 'REPORTID' 465 | if 'PROBTYPE' in layer_fields: 466 | report_type.value = 'PROBTYPE' 467 | if 'submitdt' in layer_fields: 468 | report_opendate.value = 'submitdt' 469 | 470 | if ftables.value: 471 | flag_fld.filter.list = list(set(layer_fields) & set(table_fields)) 472 | else: 473 | flag_fld.filter.list = layer_fields 474 | 475 | # If tables are changed 476 | if not ftables.value: 477 | tb_flds.enabled = False 478 | table_fields = [] 479 | 480 | elif ftables.valueAsText != table_list: 481 | tb_flds.enabled = True 482 | table_fields = [] 483 | table_list = ftables.valueAsText 484 | flag_fld.filter.list = layer_fields 485 | 486 | services = [item.split(' ')[-1][1:-2] for item in str(ftables.value).split(';')] 487 | 488 | for url in services: 489 | tab = FeatureLayer(url, gis) 490 | new_fields = [field['name'] for field in tab.properties.fields] 491 | 492 | if table_fields: 493 | table_fields = list(set(new_fields) & set(table_fields)) 494 | else: 495 | table_fields = new_fields 496 | 497 | tb_flds.filters[0].list = table_fields 498 | 499 | if flayers.value: 500 | flag_fld.filter.list = list(set(table_fields) & set(layer_fields)) 501 | else: 502 | flag_fld.filter.list = table_fields 503 | 504 | return 505 | 506 | def updateMessages(self, parameters): 507 | """Modify the messages created by internal validation for each tool 508 | parameter. This method is called after internal validation.""" 509 | return 510 | 511 | def execute(self, parameters, messages): 512 | """The source code of the tool.""" 513 | portal_url, portal_user, portal_pw, cw_url, cw_user, cw_pw, cw_timezone, cw_cwol, group, flayers, cw_id, report_id, cw_probtype, report_type, cw_opendate, report_opendate, flag_fld, flag_on, flag_off, fl_flds, ftables, tb_flds, config_path = parameters 514 | 515 | layer_urls = [item.split(' ')[-1][1:-2] for item in str(flayers.value).split(';')] 516 | table_urls = [item.split(' ')[-1][1:-2] for item in str(ftables.value).split(';')] 517 | layer_fields = [[field[1], field[0]] for field in fl_flds.value] 518 | table_fields = [] 519 | if tb_flds.value != None: 520 | table_fields = [[field[1], field[0]] for field in tb_flds.value] 521 | 522 | if table_urls[0] == 'o': 523 | table_urls = [] 524 | cfg = {} 525 | cfg['cityworks'] = {'url': cw_url.value, 526 | 'username': cw_user.value, 527 | 'password': cw_pw.value, 528 | 'timezone': cw_timezone.value, 529 | 'isCWOL': cw_cwol.value} 530 | cfg['arcgis'] = {'url': portal_url.value, 531 | 'username': portal_user.value, 532 | 'password': portal_pw.value, 533 | 'layers': layer_urls, 534 | 'tables': table_urls} 535 | cfg['fields'] = {'layers': layer_fields, 536 | 'tables': table_fields, 537 | 'ids': [cw_id.value, report_id.value], 538 | 'type': [cw_probtype.value, report_type.value], 539 | 'opendate': [cw_opendate.value, report_opendate.value]} 540 | cfg['flag'] = {'field': flag_fld.value, 541 | 'on': flag_on.value, 542 | 'off': flag_off.value} 543 | with open(config_path.valueAsText, 'w') as cfgfile: 544 | json.dump(cfg, cfgfile, indent=4) 545 | 546 | return 547 | -------------------------------------------------------------------------------- /CityworksConnection/connect_to_cityworks.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: connect_to_cityworks.py 3 | # Purpose: Pass reports from esri to cityworks 4 | # 5 | # Author: alli6394 6 | # 7 | # Created: 31/10/2016 8 | # 9 | # Version: Unreleased 10 | 11 | # Copyright 2016 Esri 12 | 13 | # Licensed under the Apache License, Version 2.0 (the "License"); 14 | # you may not use this file except in compliance with the License. 15 | # You may obtain a copy of the License at 16 | # 17 | # http://www.apache.org/licenses/LICENSE-2.0 18 | # 19 | # Unless required by applicable law or agreed to in writing, software 20 | # distributed under the License is distributed on an "AS IS" BASIS, 21 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | # See the License for the specific language governing permissions and 23 | # limitations under the License. 24 | 25 | # ------------------------------------------------------------------------------ 26 | 27 | from arcgis.gis import GIS # , Group, Layer 28 | from arcgis.features import FeatureLayer # , Table 29 | 30 | import requests 31 | import json 32 | from os import path, remove 33 | from datetime import datetime 34 | from dateutil.tz import gettz 35 | from dateutil.parser import parse 36 | 37 | cw_token = "" 38 | baseUrl = "" 39 | log_to_file = True 40 | 41 | 42 | def get_response(url, params): 43 | response = requests.post(url, params=params) 44 | try: 45 | return json.loads(response.text) 46 | except: 47 | return {'ErrorMessages':'HTML returned, check {}/Errors.axd'.format(baseUrl)} 48 | 49 | 50 | def get_cw_token(user, pwd, isCWOL): 51 | """Retrieve a token for Cityworks access""" 52 | if isCWOL: 53 | data = {"LoginName": user, "Password": pwd} 54 | json_data = json.dumps(data, separators=(",", ":")) 55 | params = {"data": json_data} 56 | url = "https://login.cityworksonline.com/Services/General/Authentication/CityworksOnlineAuthenticate" 57 | 58 | response = get_response(url, params) 59 | 60 | if response["Status"] is not 0: 61 | return "error: {}: {}".format(response["Status"], 62 | response["Message"]) 63 | else: 64 | pwd = response["Value"]["Token"] 65 | data = {"LoginName": user, "Password": pwd} 66 | json_data = json.dumps(data, separators=(",", ":")) 67 | params = {"data": json_data} 68 | url = "{}/Services/General/Authentication/Authenticate".format(baseUrl) 69 | 70 | response = get_response(url, params) 71 | 72 | if response["Status"] is not 0: 73 | return "error: {}: {}".format(response["Status"], 74 | response["Message"]) 75 | else: 76 | global cw_token 77 | cw_token = response["Value"]["Token"] 78 | 79 | return "success" 80 | 81 | 82 | def get_wkid(): 83 | """Retrieve the WKID of the cityworks layers""" 84 | 85 | params = {"token": cw_token} 86 | url = "{}/Services/AMS/Preferences/User".format(baseUrl) 87 | 88 | response = get_response(url, params) 89 | 90 | try: 91 | return response["Value"]["SpatialReference"] 92 | 93 | except KeyError: 94 | return "error" 95 | 96 | 97 | def get_problem_types(): 98 | """Retrieve a dict of problem types from cityworks""" 99 | 100 | data = {"ForPublicOnly": "true"} 101 | json_data = json.dumps(data, separators=(",", ":")) 102 | params = {"data": json_data, "token": cw_token} 103 | url = "{}/Services/AMS/ServiceRequest/Problems".format(baseUrl) 104 | 105 | try: 106 | response = get_response(url, params) 107 | 108 | values = {} 109 | for val in response["Value"]: 110 | values[val["ProblemCode"].upper()] = int(val["ProblemSid"]) 111 | 112 | return values 113 | 114 | except Exception as error: 115 | return "error: " + str(error) 116 | 117 | 118 | def submit_to_cw(row, prob_types, fields, oid, typefields): 119 | 120 | attrs = row.attributes 121 | geometry = row.geometry 122 | 123 | try: 124 | prob_sid = prob_types[attrs[typefields[1]].upper()] 125 | 126 | except KeyError: 127 | if attrs[typefields[1]].strip() == "": 128 | msg = "WARNING: No problem type provided. Record {} not exported.".format(oid) 129 | return msg 130 | else: 131 | ptype = attrs[typefields[1]] 132 | msg = "WARNING: Problem type {} not found in Cityworks. Record {} not exported.".format(ptype, oid) 133 | return msg 134 | 135 | except AttributeError: 136 | msg = "WARNING: Record {} not exported due to missing value in field {}".format(oid, typefields[1]) 137 | return msg 138 | 139 | except Exception as e: 140 | msg = "WARNING: Record {} not exported. Unknown issue getting problem type: {}".format(oid, e.message) 141 | return msg 142 | 143 | # Build dictionary of values to submit to CW 144 | values = {} 145 | for fieldset in fields: 146 | c_field, a_field = fieldset 147 | values[c_field] = str(attrs[a_field]) 148 | values["X"] = geometry["x"] 149 | values["Y"] = geometry["y"] 150 | values[typefields[0]] = prob_sid 151 | values["InitiatedByApp"] = "Crowdsource Reporter" 152 | 153 | # Convert dict to pretty print json 154 | json_data = json.dumps(values, separators=(",", ":")) 155 | params = {"data": json_data, "token": cw_token} 156 | 157 | # Submit report to Cityworks. 158 | url = "{}/Services/AMS/ServiceRequest/Create".format(baseUrl) 159 | 160 | response = get_response(url, params) 161 | try: 162 | return response["Value"] 163 | 164 | except TypeError: 165 | try: 166 | return 'error: {}'.format(response['ErrorMessages']) 167 | except KeyError: 168 | return 'error: {}'.format(response['Message']) 169 | except Exception: 170 | return 'error: {}'.format(response) 171 | 172 | 173 | def copy_attachment(attachmentmgr, attachment, oid, requestid): 174 | 175 | # download attachment 176 | attpath = attachmentmgr.download(oid, attachment["id"]) 177 | 178 | # upload attachment 179 | file = open(attpath[0], "rb") 180 | data = {"RequestId": requestid} 181 | json_data = json.dumps(data, separators=(",", ":")) 182 | params = {"token": cw_token, "data": json_data} 183 | files = {"file": (path.basename(attpath[0]), file)} 184 | url = "{}/Services/AMS/Attachments/AddRequestAttachment".format(baseUrl) 185 | response = requests.post(url, files=files, data=params) 186 | 187 | # delete downloaded file 188 | file.close() 189 | remove(attpath[0]) 190 | 191 | return json.loads(response.text) 192 | 193 | 194 | def copy_comments(record, parent, fields, ids): 195 | 196 | values = {ids[0]: parent.attributes[ids[1]]} 197 | for field in fields: 198 | values[field[0]] = record.attributes[field[1]] 199 | 200 | json_data = json.dumps(values, separators=(",", ":")) 201 | params = {"data": json_data, "token": cw_token} 202 | url = "{}/Services/AMS/CustomerCall/AddToRequest".format(baseUrl) 203 | try: 204 | response = get_response(url, params) 205 | return response 206 | 207 | except json.decoder.JSONDecodeError: 208 | return 'error' 209 | 210 | 211 | def get_parent(lyr, pkey_fld, record, fkey_fld): 212 | 213 | sql = "{} = '{}'".format(pkey_fld, record.attributes[fkey_fld]) 214 | parents = lyr.query(where=sql) 215 | return parents.features[0] 216 | 217 | 218 | def main(event, context): 219 | import sys 220 | 221 | # Cityworks settings 222 | global baseUrl 223 | baseUrl = event["cityworks"]["url"] 224 | cwUser = event["cityworks"]["username"] 225 | cwPwd = event["cityworks"]["password"] 226 | timezone = event["cityworks"].get("timezone", "") 227 | isCWOL = event["cityworks"].get("isCWOL", False) 228 | 229 | # ArcGIS Online/Portal settings 230 | orgUrl = event["arcgis"]["url"] 231 | username = event["arcgis"]["username"] 232 | password = event["arcgis"]["password"] 233 | layers = event["arcgis"]["layers"] 234 | tables = event["arcgis"]["tables"] 235 | layerfields = event["fields"]["layers"] 236 | tablefields = event["fields"]["tables"] 237 | fc_flag = event["flag"]["field"] 238 | flag_values = [event["flag"]["on"], event["flag"]["off"]] 239 | ids = event["fields"]["ids"] 240 | probtypes = event["fields"]["type"] 241 | opendate = event["fields"].get("opendate", "") 242 | 243 | if log_to_file: 244 | from datetime import datetime as dt 245 | id_log = path.join(sys.path[0], "cityworks_log.log") 246 | log = open(id_log, "a") 247 | log.write("\n{} ".format(dt.now())) 248 | log.write("Sending reports to: {}\n".format(baseUrl)) 249 | else: 250 | print("Sending reports to: {}".format(baseUrl)) 251 | 252 | try: 253 | # Connect to org/portal 254 | gis = GIS(orgUrl, username, password) 255 | 256 | # Get token for CW 257 | status = get_cw_token(cwUser, cwPwd, isCWOL) 258 | 259 | if "error" in status: 260 | if log_to_file: 261 | log.write("Failed to get Cityworks token. {}\n".format(status)) 262 | else: 263 | print("Failed to get Cityworks token. {}".format(status)) 264 | raise Exception("Failed to get Cityworks token. {}".format(status)) 265 | 266 | # get wkid 267 | sr = get_wkid() 268 | 269 | if sr == "error": 270 | if log_to_file: 271 | log.write("Spatial reference not defined\n") 272 | else: 273 | print("Spatial reference not defined") 274 | raise Exception("Spatial reference not defined") 275 | 276 | # get problem types 277 | prob_types = get_problem_types() 278 | 279 | if prob_types == "error": 280 | if log_to_file: 281 | log.write("Problem types not defined\n") 282 | else: 283 | print("Problem types not defined") 284 | raise Exception("Problem types not defined") 285 | 286 | for layer in layers: 287 | lyr = FeatureLayer(layer, gis=gis) 288 | oid_fld = lyr.properties.objectIdField 289 | lyrname = lyr.properties["name"] 290 | 291 | # Get related table URL 292 | reltable = "" 293 | try: 294 | for relate in lyr.properties.relationships: 295 | url_pieces = layer.split("/") 296 | url_pieces[-1] = str(relate["relatedTableId"]) 297 | table_url = "/".join(url_pieces) 298 | 299 | if table_url in tables: 300 | reltable = table_url 301 | break 302 | # if related tables aren't being used 303 | except AttributeError: 304 | pass 305 | 306 | # query reports 307 | sql = "{}='{}'".format(fc_flag, flag_values[0]) 308 | rows = lyr.query(where=sql, out_sr=sr) 309 | updated_rows = [] 310 | 311 | for row in rows.features: 312 | try: 313 | oid = row.attributes[oid_fld] 314 | 315 | # Submit feature to the Cityworks database 316 | request = submit_to_cw(row, prob_types, layerfields, oid, probtypes) 317 | 318 | try: 319 | reqid = request["RequestId"] 320 | initDate = int(parse(request[opendate[0]]).replace(tzinfo=gettz(timezone)).timestamp() * 1000) if opendate else "" 321 | 322 | except TypeError: 323 | if "WARNING" in request: 324 | msg = "Warning generated while copying ObjectID:{} from layer {} to Cityworks: {}".format(oid, lyrname, request) 325 | if log_to_file: 326 | log.write(msg+'\n') 327 | else: 328 | print(msg) 329 | continue 330 | elif 'error' in request: 331 | msg = "Error generated while copying ObjectID:{} from layer {} to Cityworks: {}".format(oid, lyrname, request) 332 | if log_to_file: 333 | log.write(msg+'\n') 334 | else: 335 | print(msg) 336 | continue 337 | else: 338 | msg = "Uncaught response generated while copying ObjectID:{} from layer {} to Cityworks: {}".format(oid, lyrname, request) 339 | if log_to_file: 340 | log.write(msg+'\n') 341 | else: 342 | print(msg) 343 | continue 344 | 345 | # update the record in the service so that it evaluates falsely against sql 346 | sql = "{}='{}'".format(oid_fld, oid) 347 | row_orig = lyr.query(where=sql).features[0] 348 | row_orig.attributes[fc_flag] = flag_values[1] 349 | if opendate: 350 | row_orig.attributes[opendate[1]] = initDate 351 | try: 352 | row_orig.attributes[ids[1]] = reqid 353 | except TypeError: 354 | row_orig.attributes[ids[1]] = str(reqid) 355 | 356 | # apply edits to updated row 357 | status = lyr.edit_features(updates=[row_orig]) 358 | if log_to_file: 359 | log.write("Status of updates to {}, ObjectID:{} {}\n".format(lyr.properties["name"], oid, status)) 360 | else: 361 | print("Status of updates to {}, ObjectID:{} {}".format(lyr.properties["name"], oid, status)) 362 | 363 | # attachments 364 | try: 365 | attachmentmgr = lyr.attachments 366 | attachments = attachmentmgr.get_list(oid) 367 | 368 | for attachment in attachments: 369 | response = copy_attachment(attachmentmgr, attachment, oid, reqid) 370 | if response["Status"] is not 0: 371 | try: 372 | error = response["ErrorMessages"] 373 | except KeyError: 374 | error = response["Message"] 375 | 376 | msg = "Error copying attachment from feature {} in layer {}: {}".format(oid, lyrname, error) 377 | if log_to_file: 378 | log.write(msg+'\n') 379 | else: 380 | print(msg) 381 | except RuntimeError: 382 | pass # feature layer doesn't support attachments 383 | 384 | # any other error in row execution, move on to next row 385 | except Exception as e: 386 | if log_to_file: 387 | log.write(str(e)+'\n') 388 | else: 389 | print(str(e)) 390 | continue 391 | # end of row execution 392 | # end of features execution 393 | 394 | # related records 395 | rel_records = [] 396 | #if comments tables aren't used, script will crash here 397 | try: 398 | if len(lyr.properties.relationships) > 0: 399 | # related records 400 | rellyr = FeatureLayer(reltable, gis=gis) 401 | relname = rellyr.properties["name"] 402 | pkey_fld = lyr.properties.relationships[0]["keyField"] 403 | fkey_fld = rellyr.properties.relationships[0]["keyField"] 404 | sql = "{}='{}'".format(fc_flag, flag_values[0]) 405 | rel_records = rellyr.query(where=sql) 406 | # if related tables aren't being used 407 | except AttributeError: 408 | pass 409 | except KeyError: 410 | relname = "Comments" 411 | updated_rows = [] 412 | for record in rel_records: 413 | try: 414 | rel_oid = record.attributes[oid_fld] 415 | parent = get_parent(lyr, pkey_fld, record, fkey_fld) 416 | 417 | # Process comments 418 | response = copy_comments(record, parent, tablefields, ids) 419 | 420 | if 'error' in response: 421 | if log_to_file: 422 | log.write('Error accessing comment table {}\n'.format(relname)) 423 | else: 424 | print('Error accessing comment table {}'.format(relname)) 425 | break 426 | 427 | elif response["Status"] is not 0: 428 | try: 429 | error = response["ErrorMessages"] 430 | except KeyError: 431 | error = response["Message"] 432 | msg = "Error copying record {} from {}: {}".format(rel_oid, relname, error) 433 | if log_to_file: 434 | log.write(msg+'\n') 435 | else: 436 | print(msg) 437 | continue 438 | else: 439 | record.attributes[fc_flag] = flag_values[1] 440 | try: 441 | record.attributes[ids[1]] = parent.attributes[ids[1]] 442 | except TypeError: 443 | record.attributes[ids[1]] = str(parent.attributes[ids[1]]) 444 | 445 | # apply edits to updated record 446 | status = rellyr.edit_features(updates=[record]) 447 | if log_to_file: 448 | log.write("Status of updates to {}, ObjectID:{} comments: {}\n".format(relname, rel_oid, status)) 449 | else: 450 | print("Status of updates to {}, ObjectID:{} comments: {}".format(relname, rel_oid, status)) 451 | 452 | # Upload comment attachments 453 | try: 454 | attachmentmgr = rellyr.attachments 455 | attachments = attachmentmgr.get_list(rel_oid) 456 | for attachment in attachments: 457 | response = copy_attachment(attachmentmgr, attachment, rel_oid, parent.attributes[ids[1]]) 458 | if response["Status"] is not 0: 459 | try: 460 | error = response["ErrorMessages"] 461 | except KeyError: 462 | error = response["Message"] 463 | msg = "Error copying attachment. Record {} in table {}: {}".format(rel_oid, relname, error) 464 | if log_to_file: 465 | log.write(msg+'\n') 466 | else: 467 | print(msg) 468 | except RuntimeError: 469 | pass # table doesn't support attachments 470 | 471 | # any other uncaught Exception in related record export, move on to next row 472 | except Exception as e: 473 | if log_to_file: 474 | log.write(str(e)+'\n') 475 | else: 476 | print(str(e)) 477 | continue 478 | 479 | print("Finished processing: {}".format(lyrname)) 480 | 481 | except BaseException as ex: 482 | exc_tb = sys.exc_info()[2] 483 | exc_typ = sys.exc_info()[0] 484 | 485 | print('error: {} {}, Line {}'.format(exc_typ, str(ex), exc_tb.tb_lineno)) 486 | if log_to_file: 487 | log.write('error: {} {}, Line {}'.format(exc_typ, str(ex), exc_tb.tb_lineno)) 488 | 489 | except: 490 | exc_tb = sys.exc_info()[2] 491 | exc_typ = sys.exc_info()[0] 492 | 493 | print('error: {}, Line {}'.format(exc_typ, exc_tb.tb_lineno)) 494 | if log_to_file: 495 | log.write('error: {}, Line {}'.format(exc_typ, exc_tb.tb_lineno)) 496 | 497 | finally: 498 | if log_to_file: 499 | log.close() 500 | 501 | 502 | if __name__ == "__main__": 503 | 504 | import sys 505 | 506 | configfile = sys.argv[1] 507 | 508 | with open(configfile) as configreader: 509 | config = json.load(configreader) 510 | 511 | main(config, "context") 512 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Esri 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crowdsource-reporter-scripts 2 | This repository contains a series of tools that can be used to extend the functionality of editable feature layers published to an ArcGIS Online organization or an ArcGIS portal. The scripts can be scheduled using an application such as Windows Tasks to scan the feature layers for new content. 3 | 4 | [New to Github? Get started here.]: https://guides.github.com/activities/hello-world/ 5 | [ArcGIS for Local Government maps and apps]: http://solutions.arcgis.com/local-government 6 | [Local Government GitHub repositories]: http://esri.github.io/#Local-Government 7 | [guidelines for contributing]: https://github.com/esri/contributing 8 | [LICENSE]: https://github.com/ArcGIS/crowdsource-reporter-scripts/blob/master/LICENSE.txt 9 | [Generate Report IDs]: http://solutions.arcgis.com/local-government/help/crowdsource-reporter/get-started/generate-ids/ 10 | [Moderate Reports]: http://solutions.arcgis.com/local-government/help/crowdsource-reporter/get-started/automated-moderation/ 11 | [Send Email Notifications]: http://solutions.arcgis.com/local-government/help/crowdsource-reporter/get-started/email-notification/ 12 | [Enrich Reports]: https://solutions.arcgis.com/local-government/help/crowdsource-reporter/get-started/enrich-reports/ 13 | [Download a supported version of the Service Functions script here]: http://links.esri.com/download/servicefunctions 14 | [Connect to ArcGIS Workforce]: https://solutions.arcgis.com/local-government/help/crowdsource-reporter/get-started/create-workforce-assignments/ 15 | [Download a supported version of the Connect to ArcGIS Workforce script here]: http://links.esri.com/localgovernment/download/CreateWorkforceAssignments 16 | 17 | ## Generate Report IDs 18 | 19 | A script to generate identifiers for features using a defined sequence and a value that increments on a defined interval. 20 | 21 | ##### Requirements 22 | Python 2.7, ArcPy 23 | 24 | ##### Configuration 25 | For more information on configuring this script, see the documentation: [Generate Report IDs][] 26 | 27 | [Download a supported version of the Service Functions script here][], which contains the Generate Report IDs script. 28 | 29 | ## Moderate Reports 30 | 31 | A script to filter features based on lists of explicit and sensitive words. The script updates a flag field when a word on these lists is detected so that the feature no longer meets the requirements of a filter on a layer in a web map. 32 | 33 | ##### Requirements 34 | Python 2.7, ArcPy 35 | 36 | ##### Configuration 37 | For more information on configuring this script, see the documentation: [Moderate Reports][] 38 | 39 | [Download a supported version of the Service Functions script here][], which contains the Moderate Reports script. 40 | 41 | ## Send Email Notifications 42 | 43 | Send emails to specific email addresses, or to addresses in fields in the data. Multiple messages can be configured for each layer based on attribute values. The script updates a flag field to indicate that each message has been sent. 44 | 45 | ##### Requirements 46 | Python 3, ArcGIS API for Python 47 | 48 | ##### Configuration 49 | For more information on configuring this script, see the documentation: [Send Email Notifications][] 50 | 51 | [Download a supported version of the Service Functions script here][], which contains the Send Email Notifications script. 52 | 53 | ## Enrich Reports 54 | 55 | Calculate feature attributes based on the attributes of co-incident features. 56 | 57 | ##### Requirements 58 | Python 3, ArcGIS API for Python 59 | 60 | ##### Configuration 61 | For more information on configuring this script, see the documentation: [Enrich Reports][] 62 | 63 | [Download a supported version of the Service Functions script here][], which contains the Enrich Reports script. 64 | 65 | ## Connect to ArcGIS Workforce 66 | 67 | Create workforce assignments from incoming Crowdsource Reporter, GeoForm, and Survey 123 reports. 68 | 69 | ##### Requirements 70 | Python 3, ArcGIS API for Python 71 | 72 | ##### Configuration 73 | For more information on configuring this script, see the documentation: [Connect to ArcGIS Workforce][] 74 | 75 | [Download a supported version of the Connect to ArcGIS Workforce script here][] 76 | 77 | 78 | ## Cityworks Connection (Developed in Partnership with Cityworks) 79 | 80 | A script to pass data from editable ArcGIS feature layers to Cityworks tables, including related records and attachments. The script also passes the Cityworks Request ID and open date back to the ArcGIS feature. 81 | 82 | The script assumes that the data is being collected using the Crowdsource Reporter application. For input, it requires the group containing the maps that are visible in the Crowdsource Reporter app. 83 | 84 | Note: This integration requires specific versions of the Cityworks platform and integration with existing service request content. 85 | If you would like to integrate Citizen Problem Reporter with your Cityworks implementation, please reach out to your Cityworks account representative. They will be able to help you with specific system requirements and the steps required to complete the integration. 86 | 87 | ##### Requirements 88 | ArcGIS Pro 2.2+ Python 3.5+, ArcGIS API for Python 1.4.1+ 89 | 90 | ##### Configuration 91 | 1. If not previously installed, use the Python Package Manager in ArcGIS Pro to install the ArcGIS API for Python (package name: arcgis) 92 | 2. In ArcGIS Pro, install the [Solutions Deployment Tool](http://solutions.arcgis.com/shared/help/deployment-tool/), and use it to deploy the Citizen Problem Reporter solution to your portal or organization. If necessary, use this tool to add fields to the layers and to update the domains to match the report types found in Cityworks. 93 | 3. In ArcGIS Pro, add the Connect2Cityworks toolbox to your current project. 94 | 4. Open the tool contained in the toolbox. 95 | 5. Provide the connection information for both Cityworks and ArcGIS Online/ArcGIS Enterprise. 96 | 6. Choose the group used to configure the Crowdsource Reporter application being used to collect the data. 97 | 7. Choose the layers that the tool should process. These parameters will list all layers from the maps in the group that have at least one editable field configured in the popup. 98 | 8. Choose the Cityworks field that contains the Request ID, and the ArcGIS field where this value should be recorded. Only ArcGIS fields found in all selected layers will be options for the ArcGIS Report ID Field parameter. 99 | 9. Choose the Cityworks field that contains the open date of the service request, and the ArcGIS field where this value should be recorded. Only ArcGIS fields found in all selected layers will be options for the ArcGIS Open Date Field parameter. 100 | 10. Choose the Cityworks field and the ArcGIS field that contains the type of request generated by the report. Both fields must support identical values. Only ArcGIS fields found in all selected layers will be options for the ArcGIS Report Type Field parameter. 101 | 11. Choose the field used to indicate the status of the report with regards to being transferred to Cityworks. Specify the value of this field when the report needs to be transferred in the Flag On Value parameter, and the value once the transfer is complete in the Flag Off Value parameter. 102 | 12. Map other field pairs to transfer additional information received with the report to Cityworks. Some Cityworks fields you will want to consider are: CallerFirstName, CallerLastName, CallerAddress, CallerCity, CallerState, CallerZip, CallerHomePhone, CallerEmail, Details, Address. Map an unused Cityworks Universal Custom Field to the ArcGIS field OBJECTID. For example Num5 to OBJECTID. 103 | 13. Optionally, choose editable tables from the maps to process as well as the layers. These related records will be copied to Cityworks as additional information on the report. As with the layers, map the ArcGIS and Cityworks fields to specify which data should be transferred. To transfer comments, map the following Cityworks fields: FirstName, LastName, Comments. 104 | 14. Finally, choose a location to save out this information in a configuration file. This configuration file will be read by the script that will handle the transfer of information between ArcGIS and Cityworks. 105 | 106 | The following is an example of what a config file might look like: 107 | ``` 108 | { 109 | "cityworks": { 110 | "username": "apiuser", 111 | "password": "apipassword", 112 | "url": "https://cityworks.your-org.com/CityworksSite", 113 | "timezone": "America/Denver", 114 | "isCWOL": false 115 | }, 116 | "fields": { 117 | "ids": [ 118 | "RequestId", 119 | "REPORTID" 120 | ], 121 | "type": [ 122 | "ProblemSid", 123 | "PROBTYPE" 124 | ], 125 | "opendate": [ 126 | "DateTimeInit", 127 | "submitdt" 128 | ], 129 | "layers": [ 130 | [ 131 | "CallerAddress", 132 | "ADDRESS" 133 | ], 134 | [ 135 | "CallerCity", 136 | "CITY" 137 | ], 138 | [ 139 | "CallerState", 140 | "STATE" 141 | ], 142 | [ 143 | "CallerZip", 144 | "ZIP" 145 | ], 146 | [ 147 | "CallerFirstName", 148 | "FNAME" 149 | ], 150 | [ 151 | "CallerLastName", 152 | "LNAME" 153 | ], 154 | [ 155 | "CallerHomePhone", 156 | "PHONE" 157 | ], 158 | [ 159 | "CallerEmail", 160 | "EMAIL" 161 | ], 162 | [ 163 | "Details", 164 | "DETAILS" 165 | ], 166 | [ 167 | "Address", 168 | "LOCATION" 169 | ], 170 | [ 171 | "Num5", 172 | "OBJECTID" 173 | ] 174 | ], 175 | "tables": [ 176 | [ 177 | "FirstName", 178 | "FNAME" 179 | ], 180 | [ 181 | "LastName", 182 | "LNAME" 183 | ], 184 | [ 185 | "Comments", 186 | "COMMENT" 187 | ] 188 | ] 189 | }, 190 | "flag": { 191 | "field": "processed", 192 | "on": "No", 193 | "off": "Yes" 194 | }, 195 | "arcgis": { 196 | "username": "ArcGISUser", 197 | "password": "ArcGISPassword", 198 | "layers": [ 199 | "https://services.arcgis.com/5555555555555555/arcgis/rest/services/Citizen_Request_Portal/FeatureServer/2", 200 | "https://services.arcgis.com/5555555555555555/arcgis/rest/services/Citizen_Request_Portal/FeatureServer/1", 201 | "https://services.arcgis.com/5555555555555555/arcgis/rest/services/Citizen_Request_Portal/FeatureServer/0" 202 | ], 203 | "tables": [ 204 | "https://services.arcgis.com/5555555555555555/arcgis/rest/services/Citizen_Request_Portal/FeatureServer/4", 205 | "https://services.arcgis.com/5555555555555555/arcgis/rest/services/Citizen_Request_Portal/FeatureServer/3", 206 | "https://services.arcgis.com/5555555555555555/arcgis/rest/services/Citizen_Request_Portal/FeatureServer/5" 207 | ], 208 | "url": "https://yourorg.maps.arcgis.com" 209 | } 210 | } 211 | ``` 212 | 213 | To execute the script that transfers data between ArcGIS and Cityworks, configure an application such as Windows Task Scheduler. 214 | 215 | 1. Open Windows Task Scheduler 216 | 2. Click Action > Create Task and provide a name for the task. 217 | 3. Click the Action tab and click New. 218 | 4. Set Action to Start a Program. 219 | 5. Browse to the location of your Python 3 installation (for example, \Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-python3\python.exe). 220 | 6. In the Add arguments text box, copy the name of the script (connect_to_cityworks.py) and the path to the configuration file save from running the tool in ArcGIS Pro.The script name and the configuration file path must be separated by a script, and the configuration file path must be surrounded with double quotes if it contains any spaces. 221 | 7. In the Start in text box, type the path to the folder containing the scripts and email templates and click OK. 222 | 8. Click the Trigger tab, click New, and set a schedule for your task. 223 | 9. Click OK. 224 | 225 | 226 | ## General Help 227 | * [New to Github? Get started here.][] 228 | 229 | ## Resources 230 | 231 | Learn more about Esri's [ArcGIS for Local Government maps and apps][]. 232 | 233 | Show me a list of other [Local Government GitHub repositories][]. 234 | 235 | ## Issues 236 | 237 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 238 | 239 | ## Contributing 240 | 241 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing][]. 242 | 243 | ## Licensing 244 | 245 | Copyright 2016 Esri 246 | 247 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 248 | 249 | http://www.apache.org/licenses/LICENSE-2.0 250 | 251 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 252 | 253 | A copy of the license is available in the repository's [LICENSE][] file. 254 | -------------------------------------------------------------------------------- /ServiceSupport.Emails.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180116124117001.0TRUE2018021595201001500000005000c:\program files\arcgis\pro\Resources\Help\gp<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer that will trigger emails to be sent.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer that will trigger emails to be sent.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Delete all email configurations for this layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Delete all email configurations for this layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Emails will be sent in the order listed. For each email setting, provide the following information:</SPAN></P><P><SPAN>Email Template: HMTL template that defines the structure and content of the email. </SPAN></P><P><SPAN>SQL Query: SQL Query that defines which features will trigger this email to be sent. At a minimum, it is recommended that this query exclude features that have the indicated Sent Values. </SPAN></P><P><SPAN>Recipient Email Address: Either a static address to which the email should be sent, or the name of a field that contains the email address.</SPAN></P><P><SPAN>Email Subject: The subject line of the email.</SPAN></P><P><SPAN>Field to Update: This field will be updated with the provided Sent Value once this email has been attempted to be sent.</SPAN></P><P><SPAN>Sent Value: The value used to indicate when an attempt has been made to deliver this email. This value will be calculated even is an error occurs when sending the email, such as an invalid email address.</SPAN></P><P><SPAN /></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Emails will be sent in the order listed. For each email setting, provide the following information:</SPAN></P><P><SPAN>Email Template: HMTL template that defines the structure and content of the email. </SPAN></P><P><SPAN>SQL Query: SQL Query that defines which features will trigger this email to be sent. At a minimum, it is recommended that this query exclude features that have the indicated Sent Valus. </SPAN></P><P><SPAN>Recipient Email Address: Either a static address to which the email should be sent, or the name of a field that contains the email address.</SPAN></P><P><SPAN>Email Subject: The subject line of the email.</SPAN></P><P><SPAN>Field to Update: This field will be updated with the provided Sent Value once this email has been attempted to be sent.</SPAN></P><P><SPAN>Sent Value: The value used to indicate when an attempt has been made to deliver this email. This value will be calculated even is an error occurs when sendign the email, such as an invalid email address.</SPAN></P><P><SPAN /></P><P><SPAN /></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>URL of the SMTP server used for sending emails.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>URL of the SMTP server used for sending emails. </SPAN></P><P><SPAN /></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>The username required to authenticate to the SMTP server. This is not required if authenticating through a port.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>The username required to authenticate to the SMTP server. This is not required if authenticating through a port.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>The password required to authenticate to the SMTP server. This is not required if authenticating through a port.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>The password required to authenticate to the SMTP server. This is not required if authenticating through a port.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>The address from which the emails should be sent.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>The address from which the emails should be sent.</SPAN></P></DIV><DIV><P><SPAN /></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>The address that should be used for any replies to the email message.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>The address that should be used for any replies sto the email message.</SPAN></P><P><SPAN /></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>Enable or disable TLS.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>Enable or disable TLS.</SPAN></P></DIV><DIV><P><SPAN /></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>To add attributes from the feature prompting the email to the body or subject of the message, specify text strings that appear in the body or subject and the field or text that should replace them. For example, to add the ID of a feature from the REQUESTID field to the email subject, include a piece of text such as {ID} in the configured Email Subject, and specify here that {ID} should be replaced with the value of the REQUESTID field. All specified substitutions are applied to both the email subject and the email body, for all emails configured for all layers.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>To add attributes from the feature prompting the email to the body or subject of the message, specify text strings that appear in the body or subject and the field or text that should replace them. For example, to add the ID of a feature from the REQUESTID field to the email subject, include a piece of text such as {ID} in the configured Email Subject, and specify here that {ID} should be replaced with the value of the REQUESTID field. All specified substitutions are applied to both the email subject and the email body, for all emails configured for all layers.</SPAN></P></DIV><DIV><P><SPAN /></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Send emails based on the attributes of features and table records in ArcGIS Online or Portal-managed layers. These emails can be triggered based on user-provided attributes, or attributes calculated by other tools, such as the Enrich Reports and Moderate Reports tools and can be internal to your organization, or used as a way to acknoledge receipt of a report.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Trigger emails to be sent to either a static email address or one included as an attribute. Emails can be send based on specific attributes by specifying an SQL query that selects only the features for which an email should be sent. </SPAN></P><P><SPAN>The contents of the email body can be built in an HTML template, and both the email body and the email subject support including attribute values from the feature or record that is generating the email.</SPAN></P></DIV></DIV></DIV>Send EmailsEsri., Inc.email; notificationArcToolbox Tool20180215 3 | -------------------------------------------------------------------------------- /ServiceSupport.Enrich.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180116124055001.0TRUE2018021594107001500000005000c:\program files\arcgis\pro\Resources\Help\gp<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer on which attributes will be calculated.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer on which attributes will be calculated.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Existing enrichment configurations for the selected layer. Choose an existing configuration to update the properties or to delete the configuration. Choose 'Add New' to configure an additional enrichment layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Existing enrichment configurations for the selected layer. Choose an existing configuration to update the properties or to delete the configuration. Choose 'Add New' to configure an additional enrichment layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>When checked, the currently selected enrichment configuration will be deleted for the currently selected layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>When checked, the currently selected enrichment configuration will be deleted for the currently selected layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Polygon feature layer from which attributes will be copied.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Polygon feature layer from which attributes will be copied.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>Field in the enrichment layer containing the value that will be copied to a field in the target layer.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><P><SPAN>Field in the enrichment layer containing the value that will be copied to a field in the target layer.</SPAN></P></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>Field in the target layer that will be populated with the value from a field on the enrichment layer. Features already containing a value in this field will not be processed.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>Field in the target layer that will be populated with the value from a field on the enrichment layer. Features already containing a value in this field will not be processed.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Enrichment configurations for a layer that have a lower priority value will override enrichment configuration with a higher value. </SPAN></P><P><SPAN>For example, if a layer has two enrichment configurations that both target the same field, and one has a priority value of </SPAN><SPAN STYLE="font-weight:bold;">1</SPAN><SPAN> and one has a priority value of </SPAN><SPAN STYLE="font-weight:bold;">2</SPAN><SPAN>, the script will attempt to populate the field with a value from the configuration with priority </SPAN><SPAN STYLE="font-weight:bold;">1</SPAN><SPAN>, and if there is no value it will fall back to the configuration that has priority </SPAN><SPAN STYLE="font-weight:bold;">2</SPAN><SPAN>.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Enrichment configurations for a layer that have a lower priority value will override enrichment configuration with a higher value. </SPAN></P><P><SPAN>For example, if a layer has two enrichment configurations that both target the same field, and one has a priority value of </SPAN><SPAN STYLE="font-weight:bold;">1</SPAN><SPAN> and one has a priority value of </SPAN><SPAN STYLE="font-weight:bold;">2</SPAN><SPAN>, the script will attempt to populate the field with a value from the configuration with priority </SPAN><SPAN STYLE="font-weight:bold;">1</SPAN><SPAN>, and if there is no value it will fall back to the configuration that has priority </SPAN><SPAN STYLE="font-weight:bold;">2</SPAN><SPAN>.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Calculate attributes for new features based on the attributes of co-incident features.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Use this tool to ensure complete attribution for new features by populating attribute values using values from co-incident features. For example, populate incoming Public Works service requests with the name and contact information of the local public works district based on the attributes of co-incident polygons representing those districts. This contact information can then be used to send an email to the person responsible for responding to the issue.</SPAN></P><P><SPAN>Many enrichment layers and fields can be configured for a single target layer by running the tol multiple times with the same input layer. Once a configuration has been created, it can be editied or deleted at any time by selecting the same input layer and choosing the correct configuration from the Enrichment Configurations list.</SPAN></P><P /></DIV></DIV></DIV>Enrich Reportspoint in polygon; enrich; geoattributesEsri., Inc.ArcToolbox Tool20180215 3 | -------------------------------------------------------------------------------- /ServiceSupport.General.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180116122811001.0TRUE20180327121949001500000005000c:\program files\arcgis\pro\Resources\Help\gp<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>URL to an ArcGIS Online organization, or an ArcGIS Enterprise portal.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>URL to an ArcGIS Online organization, or an ArcGIS Enterprise portal.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Username to an account that will have editing access to the services that will be configured usign the other tools in this toolbox.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Username to an account that will have editing access to the services that will be configured usign the other tools in this toolbox.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Password for the provided username.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Password for the provided username.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Define the ArcGIS Enterprise portal or ArcGIS Online organization containing the services that will be configured for the other functions. </SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>The provided credentials must have editing priviledges for the services that will be configured. These values must be specified before running any of the other tools in this toolbox. Connection information can be updated at any time by re-running this tool.</SPAN></P><P><SPAN /></P></DIV></DIV></DIV>Define Connection SettingsEsri., Inc.portallocal governmentArcToolbox Tool20180327 3 | -------------------------------------------------------------------------------- /ServiceSupport.Identifiers.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180116124108001.0TRUE2018021594111001500000005000c:\program files\arcgis\pro\Resources\Help\gp<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer for which identifiers will be calculated.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer for which identifiers will be calculated.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>When checked, the identifier configuration will be deleted for the currently selected layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>When checked, the identifier configuration will be deleted for the currently selected layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Choose the identifier sequence to use for this layer. Identifier sequences can be shared accross many layers, and must be specified in the General Identifier Settings section of the tool.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Choose the identifier sequence to use for this layer. Identifier sequences can be shared accross many layers, and must be specified in the General Identifier Settings section of the tool.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><P><SPAN>Choose the field in the layer where the identifier should be stored. Only features that do not have a value in this field will have an identifier calculated.</SPAN></P></DIV></DIV><DIV STYLE="text-align:Left;"><P><SPAN>Choose the field in the layer where the identifier should be stored. Only features that do not have a value in this field will have an identifier calculated.</SPAN></P></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>For each identifier sequence, provide the followign information:</SPAN></P><P><SPAN>Sequence Name: Assign a name to the sequence. This value will appear in the drop-down menu for selecting the sequence to assign to each layer.</SPAN></P><P><SPAN>Pattern: The patter to use for the sequence. This can be a combination of letters, number, and symbols. Mark the location for the incrementing value with a pair of curly braces {}. Python formatting will be applied to the pattern text, so string formatting syntax such as {0:03d} will pad the incrementing number section with zeros to a length of 3. For example, the pattern seq-{0:05d} would result in identifier values such as 'seq-0001', 'seq-002', 'seq-0010', etc.</SPAN></P><P><SPAN>Next Value: When initially creating the sequence, this should be the first value you'd like to use in the identifiers. After this point, this value will show the value to be used for the next identifier generated.</SPAN></P><P><SPAN>Interval: The interval by which the identifier values should increase between features. for example, an initial Next Value of 1 and an interval of 10 would create identifiers with the incrementing values of 1, 11, 21, etc.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>For each identifier sequence, provide the following information:</SPAN></P><P><SPAN>Sequence Name: Assign a name to the sequence. This value will appear in the drop-down menu for selecting the sequence to assign to each layer.</SPAN></P><P><SPAN>Pattern: The patter to use for the sequence. This can be a combination of letters, number, and symbols. Mark the location for the incrementing value with a pair of curly braces {}. Python formatting will be applied to the pattern text, so string formatting syntax such as {0:03d} will pad the incrementing number section with zeros to a length of 3. For example, the pattern seq-{0:05d} would result in identifier values such as 'seq-0001', 'seq-002', 'seq-0010', etc.</SPAN></P><P><SPAN>Next Value: When initially creating the sequence, this should be the first value you'd like to use in the identifiers. After this point, this value will show the value to be used for the next identifier generated.</SPAN></P><P><SPAN>Interval: The interval by which the identifier values should increase between features. for example, an initial Next Value of 1 and an interval of 10 would create identifiers with the incrementing values of 1, 11, 21, etc.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Calculate a custom-formatted identifier for each feature in a service to help distinguish between incoming reports.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>The same identifier sequences may be used for features in many layers. Establish the identifier sequences in the General Identifier Settings section of the tool, and then reference the sequence when configuring each layer for which identifiers should be calculated.</SPAN></P><P><SPAN>Identifiers will be calculated for all features in the layer that do not have a value in the configured ID field.</SPAN></P><P><SPAN>Identifier settings can be updated or deleted for a layer at any time by selecting the layer and modifying the ID Sequence and ID Field values. Only one ID Sequence and ID Field can be stored for each layer. Removing or editing Identifier Sequences should be done very carefully as this will impact any layers that are currently using the sequence. </SPAN></P><P><SPAN>To avoid the chance of generating duplicate sequences, pause the task that runs the servicefunctions.py script when adding, modifying, or deleting any identifier settings using this tool.</SPAN></P></DIV></DIV></DIV>Generate IDsEsri., Inc.identifiersidsArcToolbox Tool20180215 3 | -------------------------------------------------------------------------------- /ServiceSupport.Moderate.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180116124113001.0TRUE2018021594843001500000005000c:\program files\arcgis\pro\Resources\Help\gp<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer to be moderated.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Point, line, or polygon feature layer to be moderated.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Choose to add a new moderation configuration to this layer, or to edit an existing configuration.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Choose to add a new moderation configuration to this layer, or to edit an existing configuration.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>When checked, the selected moderation configuration will be deleted for the currently selected layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>When checked, the selected moderation configuration will be deleted for the currently selected layer.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Choose the moderation list to use for this layer. Moderation lists can be shared accross many layers, and each layer can have multiple lists configured but they must configured individually. These moderation lists must be specified in the General Moderation Settings section of the tool.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Choose the moderation list to use for this layer. Moderation lists can be shared accross many layers, and each layer can have multiple lists configured but they must configured individually. These moderation lists must be specified in the General Moderation Settings section of the tool.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Select the fields that will be monitored for content that matches the words and phrases from the specified moderation list.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Select the fields that will be monitored for content that matches the words and phrases from the specified moderation list.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>SQL statement used to select the features that will be moderated. If no SQL statement is provided, all features will be processed every time the script runs. </SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>SQL statement used to select the features that will be moderated. If no SQL statement is provided, all features will be processed every time the script runs.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Field that will be updated when a feature is found that contains at least one of the words or phrases in the moderation list.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Field that will be updated when a feature is found that contains at least one of the words or phrases in the moderation list.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Value that will be calculated for the specified field when a feature is found that contains at least one of the words or phrases in the moderation list.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>a feature is found that contains at least one of the words or phrases in the moderation list.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>For each moderation list, provide the following information:</SPAN></P><P><SPAN>List Name: Assign a name to the moderation list. This value will appear in the drop-down menu for selecting the moderation list use when scanning each layer.</SPAN></P><P><SPAN>Filter Type: Choose to scan feature for words and phrases that exactly match the provided list of works and phrases. For example, when the filter type is EXACT, if the list contains the word 'duck' the script will update the specified field when the feature contains the word 'duck', but not when it contains the word 'duckling'. When the filter type is FUZZY, the script will update the feature when either 'duck' or 'duckling' are found.</SPAN></P><P><SPAN>Words and Phrases: Provide a comma-seperated list of words or phrases to scan for.</SPAN></P><P><SPAN /></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>For each moderation list, provide the following information:</SPAN></P><P><SPAN>List Name: Assign a name to the moderation list. This value will appear in the drop-down menu for selecting the moderation list use when scanning each layer.</SPAN></P><P><SPAN>Filter Type: Choose to scan feature for words and phrases that exactly match the provided list of works and phrases. For example, when the filter type is EXACT, if the list contains the word 'duck' the script will update the specified field when the feature contains the word 'duck', but not when it contains the word 'duckling'. When the filter type is FUZZY, the script will update the feature when either 'duck' or 'duckling' are found.</SPAN></P><P><SPAN>Words and Phrases: Provide a comma-seperated list of words or phrases to scan for. This list is case-insensitive.</SPAN></P><P><SPAN /></P><P><SPAN /></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Some characters are frequently substituted for others in an attempt to get around moderation filters, such as using @ instead of the letter 'a'. To take these substitutions into consideration, list each letter and the equivalent characters for that letter. This list is case-insensitive. List all substitutions for each letter on a single line, without seperators. For example, for the letter 'a', including '@&amp;' in the substitutions space will test for the letter 'a' in place of any @ or &amp; signs found, in order to match the word against the contents of the words and phrases list. This comparison takes place for both EXACT and FUZZY filter types.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Some characters are frequently substituted for others in an attempt to get around moderation filters, such as using @ instead of the letter 'a'. To take these substitutions into consideration, list each letter and the equivalent characters for that letter. This list is case-insensitive. List all substitutions for each letter on a single line, without seperators. For example, for the letter 'a', including '@&amp;' in the substitutions space will test for the letter 'a' in place of any @ or &amp; signs found, in order to match the word against the contents of the words and phrases list. This comparison takes place for both EXACT and FUZZY filter types.</SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>Moderate features ro table records by updating the value of a field if a word or phrase is found in another field. </SPAN></P></DIV></DIV></DIV><DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>This can be used in the traditional sense of moderation - to flag features and records that contain inappropriate or sensitive content. Public-facing web maps displaying these features can then be configured to hide these reports - at least until they can be reviewed by a person to confirm that they should be hidden. </SPAN></P><P><SPAN>This tool can also be used to identify key words or phrases that can be used to automatically set priority or assign reports. For example, this script can be paired up with the Enrich Reports and Send Emails tools. If a report is submitted within a specific juristiction containing specific key words, for example, an unplowed street where the description contains the words 'critical' or 'hazardous', these moderation capabilities could update the Priority attribute to High and trigger an email notification to the necessary parties to get the issue reviewed and, if necessary, resolved quickly.</SPAN></P><P><SPAN>View, update, or delete moderation configurations for any layer by selecting the layer in the tool and choosing an existing configuration from the list.</SPAN></P><P><SPAN>Removing or editing Moderation Lists and Character Substitutions should be done very carefully as this will impact any layers that are currently using these values. </SPAN></P></DIV></DIV></DIV>Moderate ReportsEsri., Inc.moderationcalculateArcToolbox Tool20180215 3 | -------------------------------------------------------------------------------- /ServiceSupport.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180116122635001.0TRUE201807231618581500000005000c:\program files\arcgis\pro\Resources\Help\gpServiceSupportA set of tools that can be scheduled to perform actions based on new or updated features in layers that are hosted in or managed by an ArcGIS Online organization or Portal for ArcGIS.<DIV STYLE="text-align:Left;"><DIV><DIV><P><SPAN>These tools generate and update a JSON configuration file that is read by the accompanying servicefunctions.py script, which actually performs the actions. The configuration file is created in the same directory as the toolbox. All tools require that a connection to the hosting ArcGIS Online Organization or Portal be defined using the Define Portal Settings tool. </SPAN></P><P><SPAN>After using these tools to establich which actions should be performed for which tools, use a program such as Windows Task Scheduler to configure the servicefunctions.py script to run on a schedule. This script and the generated JSON configuration file must exist in the same directory. To be able to edit this JSON in the future using this toolbox, the toolbox must also stay in the same directory as the configuration file.</SPAN></P></DIV></DIV></DIV>Esri., Inc.local governmentArcToolbox Toolbox20180215 3 | -------------------------------------------------------------------------------- /WorkforceConnection/New Python Toolbox.Tool.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180724140300001.0TRUE 3 | -------------------------------------------------------------------------------- /WorkforceConnection/New Python Toolbox.Workforce.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180724141328001.0TRUE 3 | -------------------------------------------------------------------------------- /WorkforceConnection/New Python Toolbox.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180724132454001.0TRUE20180726100411c:\program files\arcgis\pro\Resources\Help\gpNew Python ToolboxArcToolbox Toolbox 3 | -------------------------------------------------------------------------------- /WorkforceConnection/Workforce Connection.pyt: -------------------------------------------------------------------------------- 1 | import arcpy 2 | import json 3 | from os import path 4 | from arcgis.gis import GIS 5 | #from arcgis.apps import workforce 6 | import copy 7 | 8 | configuration_file = path.join(path.dirname(__file__), 'WorkforceConnection.json') 9 | 10 | class Toolbox(object): 11 | def __init__(self): 12 | """Define the toolbox (the name of the toolbox is the name of the 13 | .pyt file).""" 14 | self.label = "Toolbox" 15 | self.alias = "" 16 | 17 | # List of tool classes associated with this toolbox 18 | self.tools = [Workforce] 19 | 20 | 21 | class Workforce(object): 22 | def __init__(self): 23 | """Define the tool (tool name is the name of the class).""" 24 | self.label = "Configure Workforce Connection" 25 | self.description = "" 26 | self.canRunInBackground = False 27 | 28 | def getParameterInfo(self): 29 | """Define parameter definitions""" 30 | 31 | portal_url = arcpy.Parameter( 32 | displayName='ArcGIS Online organization or ArcGIS Enterprise portal URL', 33 | name='portal_url', 34 | datatype='GPString', 35 | parameterType='Required', 36 | direction='Input') 37 | portal_url.filter.type = 'ValueList' 38 | portal_url.filter.list = arcpy.ListPortalURLs() 39 | 40 | portal_user = arcpy.Parameter( 41 | displayName='Username', 42 | name='portal_user', 43 | datatype='GPString', 44 | parameterType='Required', 45 | direction='Input') 46 | 47 | portal_pass = arcpy.Parameter( 48 | displayName='Password', 49 | name='portal_pass', 50 | datatype='GPStringHidden', 51 | parameterType='Required', 52 | direction='Input') 53 | 54 | layer = arcpy.Parameter( 55 | displayName='Layer', 56 | name='layer', 57 | datatype='GPFeatureLayer', 58 | parameterType='Required', 59 | direction='Input') 60 | 61 | wkfcconfigs = arcpy.Parameter( 62 | displayName='Workforce configurations', 63 | name='wkfcconfigs', 64 | datatype='GPString', 65 | parameterType='Required', 66 | direction='Input') 67 | wkfcconfigs.filter.type = 'ValueList' 68 | wkfcconfigs.filter.list = ['Add New'] 69 | wkfcconfigs.enabled = 'False' 70 | 71 | project = arcpy.Parameter( 72 | displayName='Workforce Project', 73 | name='project', 74 | datatype='GPString', 75 | parameterType='Required', 76 | direction='Input') 77 | project.filter.type = 'ValueList' 78 | project.filter.list = ['Provide credentials to see available projects'] 79 | 80 | sql = arcpy.Parameter( 81 | displayName='SQL Query', 82 | name='sql', 83 | datatype='GPString', 84 | parameterType='Optional', 85 | direction='Input') 86 | 87 | fieldmap = arcpy.Parameter( 88 | displayName='Field Map', 89 | name='fieldmap', 90 | datatype='GPValueTable', 91 | parameterType='Required', 92 | direction='Input') 93 | fieldmap.columns = [['Field', 'Source'], 94 | ['GPString', 'Target']] 95 | fieldmap.parameterDependencies = [layer.name] 96 | fieldmap.filters[1].type = 'ValueList' 97 | fieldmap.filters[1].list = ['Description', 'Status', 'Notes', 'Priority', 'Assignment Type', 'WorkOrder ID', 'Due Date', 'WorkerID', 'Location', 'Declined Comment', 'Assigned on Date', 'Assignment Read', 'In Progress Date', 'Completed on Date', 'Declined on Date', 'Paused on Date', 'DispatcherID'] 98 | 99 | updatefield = arcpy.Parameter( 100 | displayName='Update Field', 101 | name='updatefield', 102 | datatype='Field', 103 | parameterType='Required', 104 | direction='Input') 105 | updatefield.parameterDependencies = [layer.name] 106 | 107 | updatevalue = arcpy.Parameter( 108 | displayName='Update Value', 109 | name='updatevalue', 110 | datatype='GPString', 111 | parameterType='Required', 112 | direction='Input') 113 | 114 | delete = arcpy.Parameter( 115 | displayName='Delete this workforce configuration for this layer', 116 | name='delete', 117 | datatype='Boolean', 118 | parameterType='Optional', 119 | direction='Input') 120 | delete.enabled = "False" 121 | 122 | try: 123 | with open(configuration_file, 'r') as config_params: 124 | config = json.load(config_params) 125 | portal_url.value = config["organization url"] 126 | portal_user.value = config['username'] 127 | portal_pass.value = config['password'] 128 | 129 | except FileNotFoundError: 130 | newconfig = {'username':'', 131 | 'organization url':'', 132 | 'services':[], 133 | 'password':''} 134 | with open(configuration_file, 'w') as config_params: 135 | json.dump(newconfig, config_params) 136 | 137 | if not portal_url.value: 138 | portal_url.value = arcpy.GetActivePortalURL() 139 | 140 | if portal_url.value and not portal_user.value: 141 | try: 142 | portal_user.value = arcpy.GetPortalDescription(portal_url.valueAsText)['user']['username'] 143 | except KeyError: 144 | pass 145 | 146 | params = [portal_url, portal_user, portal_pass, layer, wkfcconfigs, delete, project, sql, fieldmap, updatefield, updatevalue] 147 | 148 | return params 149 | 150 | def isLicensed(self): 151 | """Set whether tool is licensed to execute.""" 152 | return True 153 | 154 | def updateParameters(self, parameters): 155 | """Modify the values and properties of parameters before internal 156 | validation is performed. This method is called whenever a parameter 157 | has been changed.""" 158 | 159 | portal_url, portal_user, portal_pass, layer, wkfcconfigs, delete, project, sql, fieldmap, updatefield, updatevalue = parameters 160 | 161 | if layer.value and not layer.hasBeenValidated: 162 | try: 163 | val = layer.value 164 | srclyr = val.connectionProperties['connection_info']['url'] + '/' + val.connectionProperties['dataset'] 165 | except AttributeError: 166 | srclyr = layer.valueAsText 167 | 168 | with open(configuration_file, 'r') as config_params: 169 | config = json.load(config_params) 170 | existing_configs = [] 171 | global config_list 172 | config_list= [] 173 | for service in config['services']: 174 | if service['url'] == str(srclyr): 175 | config_str = "{}: {}".format(service['project'], service['sql']) 176 | existing_configs.append(config_str) 177 | config_list.append(service) 178 | 179 | if existing_configs: 180 | wkfcconfigs.value = "" 181 | wkfcconfigs.enabled = 'True' 182 | existing_configs.insert(0, 'Add New') 183 | wkfcconfigs.filter.list = existing_configs 184 | else: 185 | wkfcconfigs.filter.list = ['Add New'] 186 | wkfcconfigs.value = "Add New" 187 | wkfcconfigs.enabled = "False" 188 | delete.enabled = "False" 189 | 190 | if portal_user.value and portal_pass.value and portal_url.value and project.filter.list == ['Provide credentials to see available projects']: 191 | gis = GIS(portal_url.valueAsText, portal_user.valueAsText, portal_pass.valueAsText) 192 | search_result = gis.content.search(query="owner:{}".format(portal_user.valueAsText), item_type="Workforce Project") 193 | project.filter.list = ['{} ({})'.format(s.title, s.id) for s in search_result] 194 | 195 | if wkfcconfigs.value and not wkfcconfigs.hasBeenValidated: 196 | if wkfcconfigs.valueAsText == 'Add New' or wkfcconfigs.valueAsText == '': 197 | delete.enabled = "False" 198 | project.value = '' 199 | sql.value = '' 200 | fieldmap.value = [] 201 | updatefield.value = '' 202 | updatevalue.value = '' 203 | else: 204 | sql.value = wkfcconfigs.valueAsText.split(':')[1].strip() 205 | project.value = wkfcconfigs.valueAsText.split(':')[0].strip() 206 | for service in config_list: 207 | if service['project'] == project.value and service['sql'] == sql.value: 208 | fieldmap.values = service['fieldmap'] 209 | updatefield.value = service['update field'] 210 | updatevalue.value = service['update value'] 211 | delete.enabled = "True" 212 | break 213 | else: 214 | delete.enabled = "False" 215 | project.value = '' 216 | sql.value = '' 217 | fieldmap.value = [] 218 | updatefield.value = '' 219 | updatevalue.value = '' 220 | 221 | if not delete.hasBeenValidated: 222 | if delete.value: 223 | projectid = wkfcconfigs.valueAsText.split(':')[1].strip() 224 | sqlstr = wkfcconfigs.valueAsText.split(':')[0].strip() 225 | for service in config_list: 226 | if service['project'] == projectid and service['sql'] == sqlstr: 227 | project.value = projectid 228 | sql.value = sqlstr 229 | fieldmap.values = service['fieldmap'] 230 | updatefield.value = service['update field'] 231 | updatevalue.value = service['update value'] 232 | break 233 | 234 | fieldmap.enabled = "False" 235 | updatefield.enabled = "False" 236 | updatevalue.enabled = "False" 237 | sql.enabled = "False" 238 | project.enabled = "False" 239 | else: 240 | fieldmap.enabled = "True" 241 | updatefield.enabled = "True" 242 | updatevalue.enabled = "True" 243 | sql.enabled = "True" 244 | project.enabled = "True" 245 | return 246 | 247 | def updateMessages(self, parameters): 248 | """Modify the messages created by internal validation for each tool 249 | parameter. This method is called after internal validation.""" 250 | return 251 | 252 | def execute(self, parameters, messages): 253 | """The source code of the tool.""" 254 | 255 | portal_url, portal_user, portal_pass, layer, wkfcconfigs, delete, project, sql, fieldmap, updatefield, updatevalue = parameters 256 | 257 | try: 258 | val = layer.value 259 | srclyr = val.connectionProperties['connection_info']['url'] + '/' + val.connectionProperties['dataset'] 260 | except AttributeError: 261 | srclyr = layer.valueAsText 262 | 263 | with open(configuration_file, 'r') as config_params: 264 | config = json.load(config_params) 265 | 266 | newconfig = copy.deepcopy(config) 267 | if newconfig['services']: 268 | sqlstr = wkfcconfigs.valueAsText.split(':')[1].strip() 269 | projectid = wkfcconfigs.valueAsText.split(':')[0].strip() 270 | for service in newconfig["services"]: 271 | if service["url"] == srclyr and service['project'] == projectid and service['sql'] == sqlstr: 272 | 273 | if wkfcconfigs.value != 'Add New': 274 | newconfig['services'].remove(service) 275 | 276 | if not delete.value: 277 | newconfig["services"].append({"url": srclyr, 278 | "project": project.valueAsText, 279 | "sql": sql.valueAsText, 280 | "fieldmap": fieldmap.valueAsText, 281 | "update field": updatefield.valueAsText, 282 | "update value":updatevalue.valueAsText}) 283 | newconfig['organization url'] = portal_url.valueAsText 284 | newconfig['username'] = portal_user.valueAsText 285 | newconfig['password'] = portal_pass.valueAsText 286 | break 287 | else: 288 | newconfig["services"].append({"url": srclyr, 289 | "project": project.valueAsText, 290 | "sql": sql.valueAsText, 291 | "fieldmap": fieldmap.valueAsText, 292 | "update field": updatefield.valueAsText, 293 | "update value": updatevalue.valueAsText}) 294 | newconfig['organization url'] = portal_url.valueAsText 295 | newconfig['username'] = portal_user.valueAsText 296 | newconfig['password'] = portal_pass.valueAsText 297 | 298 | try: 299 | with open(configuration_file, 'w') as config_params: 300 | json.dump(newconfig, config_params) 301 | except: 302 | with open(configuration_file, 'w') as config_params: 303 | json.dump(config, config_params) 304 | arcpy.AddError('Failed to update configuration file.') 305 | 306 | return -------------------------------------------------------------------------------- /WorkforceConnection/Workforce Connection.pyt.xml: -------------------------------------------------------------------------------- 1 | 2 | 20180726101354001.0TRUE20180726101354c:\program files\arcgis\pro\Resources\Help\gpWorkforce ConnectionArcToolbox Toolbox 3 | -------------------------------------------------------------------------------- /WorkforceConnection/create_workforce_assignments.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: create_workforce_assignments.py 3 | # Purpose: generates identifiers for features 4 | 5 | # Copyright 2017 Esri 6 | 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | # ------------------------------------------------------------------------------ 20 | 21 | from datetime import datetime as dt 22 | from os import path, sys 23 | from arcgis.gis import GIS 24 | from arcgis.features import FeatureLayer 25 | from arcgis.apps import workforce 26 | 27 | orgURL = '' # URL to ArcGIS Online organization or ArcGIS Portal 28 | username = '' # Username of an account in the org/portal that can access and edit all services listed below 29 | password = '' # Password corresponding to the username provided above 30 | 31 | # Specify the services/ layers to monitor for reports to pass to Workforce 32 | # [{'source url': 'Reporter layer to monitor for new reports', 33 | # 'target url': 'Workforce layer where new assignments will be created base on the new reports', 34 | # 'query': 'SQL query used to identify the new reports that should be copied', 35 | # 'fields': { 36 | # 'Name of Reporter field': 'Name of Workforce field', 37 | # 'Another Reporter field to map':'to another workforce field'}, 38 | # 'update field': 'Name of field in Reporter layer tracking which reports have been copied to Workforce', 39 | # 'update value': 'Value in update field indicating that a report has already been copied.' 40 | # }, 41 | # {'source url': 'Another Reporter layer to monitor for new reports', 42 | # 'target url': '', 43 | # 'query': '', 44 | # 'fields': {}, 45 | # 'update field': '', 46 | # 'update value': '' 47 | # }] 48 | 49 | services = [{'source url': '', 50 | 'project': '', 51 | 'query': '1=1', 52 | 'fields': { 53 | '': ''}, 54 | 'update field': '', 55 | 'update value': '' 56 | }] 57 | 58 | def main(): 59 | # Create log file 60 | with open(path.join(sys.path[0], 'attr_log.log'), 'a') as log: 61 | log.write('\n{}\n'.format(dt.now())) 62 | 63 | # connect to org/portal 64 | if username: 65 | gis = GIS(orgURL, username, password) 66 | else: 67 | gis = GIS(orgURL) 68 | 69 | for service in services: 70 | try: 71 | # Connect to source and target layers 72 | fl_source = FeatureLayer(service['source url'], gis) 73 | fl_target = FeatureLayer(service['target url'], gis) 74 | 75 | # get field map 76 | fields = [[key, service['fields'][key]] for key in service['fields'].keys()] 77 | 78 | # Get source rows to copy 79 | rows = fl_source.query(service['query']) 80 | adds = [] 81 | updates = [] 82 | 83 | for row in rows: 84 | # Build dictionary of attributes & geometry in schema of target layer 85 | # Default status and priority values can be overwritten if those fields are mapped to reporter layer 86 | attributes = {'status': 0, 87 | 'priority': 0} 88 | 89 | for field in fields: 90 | attributes[field[1]] = row.attributes[field[0]] 91 | 92 | new_request = {'attributes': attributes, 93 | 'geometry': {'x': row.geometry['x'], 94 | 'y': row.geometry['y']}} 95 | adds.append(new_request) 96 | 97 | # update row to indicate record has been copied 98 | if service['update field']: 99 | row.attributes[service['update field']] = service['update value'] 100 | updates.append(row) 101 | 102 | # add records to target layer 103 | if adds: 104 | add_result = fl_target.edit_features(adds=adds) 105 | for result in add_result['updateResults']: 106 | if not result['success']: 107 | raise Exception('error {}: {}'.format(result['error']['code'], 108 | result['error']['description'])) 109 | 110 | # update records: 111 | if updates: 112 | update_result = fl_source.edit_features(updates=updates) 113 | for result in update_result['updateResults']: 114 | if not result['success']: 115 | raise Exception('error {}: {}'.format(result['error']['code'], 116 | result['error']['description'])) 117 | 118 | except Exception as ex: 119 | msg = 'Failed to copy feature from layer {}'.format(service['url']) 120 | print(ex) 121 | print(msg) 122 | log.write('{}\n{}\n'.format(msg, ex)) 123 | 124 | if __name__ == '__main__': 125 | main() 126 | -------------------------------------------------------------------------------- /internal_email_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

A new problem report has been submitted.

5 | 6 | -------------------------------------------------------------------------------- /send_email.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: send_email.py 3 | # Purpose: Send email to specified recipients 4 | 5 | # Copyright 2017 Esri 6 | 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | # ------------------------------------------------------------------------------ 20 | from email.mime.multipart import MIMEMultipart 21 | from email.mime.text import MIMEText 22 | import smtplib, sys 23 | 24 | class EmailServer(object): 25 | def __init__(self, smtp_server, smtp_username=None, smtp_password=None, use_tls=False): 26 | self._server = smtplib.SMTP(smtp_server) 27 | if use_tls: 28 | self._server.starttls() 29 | self._server.ehlo() 30 | if smtp_username and smtp_password: 31 | self._server.esmtp_features['auth'] = 'LOGIN' 32 | self._server.login(smtp_username, smtp_password) 33 | 34 | def __enter__(self): 35 | return self 36 | 37 | def send(self, from_address="", reply_to="", to_addresses=[], cc_addresses=[], bcc_addresses=[], subject="", email_body=""): 38 | msg = MIMEMultipart() 39 | msg['from'] = from_address 40 | if reply_to != "": 41 | msg['reply-to'] = reply_to 42 | if len(to_addresses) > 0: 43 | msg['to'] = ", ".join(to_addresses) 44 | if len(cc_addresses) > 0: 45 | msg['cc'] = ", ".join(cc_addresses) 46 | msg['subject'] = subject 47 | msg.attach(MIMEText(email_body, 'html')) 48 | 49 | recipients = to_addresses + cc_addresses + bcc_addresses 50 | if ('') in recipients: 51 | recipients.remove('') 52 | if len(recipients) == 0: 53 | raise Exception("You must provide at least one e-mail recipient") 54 | 55 | self._server.sendmail(from_address, recipients, msg.as_string()) 56 | 57 | def __exit__(self, exc_type, exc_value, traceback): 58 | self._server.quit() 59 | 60 | def _add_warning(message): 61 | try: 62 | import arcpy 63 | arcpy.AddWarning(message) 64 | except ImportError: 65 | print(message) 66 | 67 | def _set_result(index, value): 68 | try: 69 | import arcpy 70 | arcpy.SetParameter(index, value) 71 | except ImportError: 72 | pass 73 | 74 | if __name__ == "__main__": 75 | smtp_server = sys.argv[1] 76 | smtp_username = sys.argv[2] 77 | smtp_password = sys.argv[3] 78 | use_tls = bool(sys.argv[4]) 79 | from_address = sys.argv[5] 80 | reply_to = sys.argv[6] 81 | to_addresses = sys.argv[7].split(';') 82 | cc_addresses = sys.argv[8].split(';') 83 | bcc_addresses = sys.argv[9].split(';') 84 | subject = sys.argv[10] 85 | email_body = sys.argv[11] 86 | 87 | # Remove empty strings from addresses 88 | to_addresses[:] = (value for value in to_addresses if value != '' and value != '#') 89 | cc_addresses[:] = (value for value in cc_addresses if value != '' and value != '#') 90 | bcc_addresses[:] = (value for value in bcc_addresses if value != '' and value != '#') 91 | all_addresses = to_addresses + cc_addresses + bcc_addresses 92 | 93 | try: 94 | with EmailServer(smtp_server, smtp_username, smtp_password, use_tls) as email_server: 95 | email_server.send(from_address, reply_to, to_addresses, cc_addresses, bcc_addresses, subject, email_body) 96 | _set_result(11, True) 97 | except Exception as e: 98 | _add_warning("Failed to send e-mail. {0}".format(str(e))) 99 | _set_result(11, False) 100 | -------------------------------------------------------------------------------- /servicefunctions.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Name: calculateids.py 3 | # Purpose: generates identifiers for features 4 | 5 | # Copyright 2017 Esri 6 | 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | # ------------------------------------------------------------------------------ 20 | 21 | from send_email import EmailServer 22 | import re 23 | from datetime import datetime as dt 24 | from os import path, sys 25 | from arcgis.gis import GIS 26 | from arcgis.features import FeatureLayer 27 | import json 28 | 29 | #id_settings = {} 30 | #modlists = {} 31 | 32 | 33 | def _add_message(msg, ertype='ERROR'): 34 | print("{}: {}".format(ertype, msg)) 35 | with open(path.join(sys.path[0], 'id_log.log'), 'a') as log: 36 | log.write("{} -- {}: {}".format(dt.now(), ertype, msg)) 37 | return 38 | 39 | 40 | def _report_failures(results): 41 | for result in results['updateResults']: 42 | if not result['success']: 43 | _add_message('{}: {}'.format(result['error']['code'], result['error']['description'])) 44 | return 45 | 46 | 47 | def _get_features(feature_layer, where_clause, return_geometry=False): 48 | """Get the features for the given feature layer of a feature service. Returns a list of json features. 49 | Keyword arguments: 50 | feature_layer - The feature layer to return the features for 51 | where_clause - The expression used in the query""" 52 | 53 | total_features = [] 54 | max_record_count = feature_layer.properties['maxRecordCount'] 55 | if max_record_count < 1: 56 | max_record_count = 1000 57 | offset = 0 58 | while True: 59 | if not where_clause: 60 | where_clause = "1=1" 61 | features = feature_layer.query(where=where_clause, 62 | return_geometry=return_geometry, 63 | result_offset=offset, 64 | result_record_count=max_record_count).features 65 | total_features += features 66 | if len(features) < max_record_count: 67 | break 68 | offset += len(features) 69 | return total_features 70 | 71 | 72 | def add_identifiers(lyr, seq, fld): 73 | """Update features in an agol/portal service with id values 74 | Return next valid sequence value""" 75 | 76 | # Get features without id 77 | value = id_settings[seq]['next value'] 78 | fmt = id_settings[seq]['pattern'] 79 | interval = id_settings[seq]['interval'] 80 | 81 | rows = _get_features(lyr, """{} is null""".format(fld)) 82 | 83 | # For each feature, update id, and increment sequence value 84 | for row in rows: 85 | row.attributes[fld] = fmt.format(value) 86 | value += interval 87 | 88 | if rows: 89 | results = lyr.edit_features(updates=rows) 90 | _report_failures(results) 91 | 92 | return value 93 | 94 | 95 | def enrich_layer(source, target, settings): 96 | wkid = source.properties.extent.spatialReference.wkid 97 | 98 | sql = "{} IS NULL".format(settings['target']) 99 | if 'sql' in settings.keys(): 100 | if settings['sql'] and settings['sql'] != "1=1": 101 | sql += " AND {}".format(settings['sql']) 102 | 103 | rows = _get_features(target, sql, return_geometry=True) 104 | 105 | # Query for source polygons 106 | source_polygons = source.query(out_fields=settings['source']) 107 | 108 | for polygon in source_polygons: 109 | polyGeom = { 110 | 'geometry': polygon.geometry, 111 | 'spatialRel': 'esriSpatialRelIntersects', 112 | 'geometryType': 'esriGeometryPolygon', 113 | 'inSR': wkid 114 | } 115 | 116 | #Query find points that intersect the source polygon and that honor the sql query from settings 117 | intersectingPoints = target.query(geometry_filter=polyGeom, where=sql, out_fields=settings['target']) 118 | 119 | source_val = polygon.get_value(settings['source']) 120 | 121 | #Set all of the intersecting points values 122 | for feature in intersectingPoints: 123 | feature.set_value(settings['target'],source_val) 124 | 125 | #Send edits if they exist 126 | if intersectingPoints: 127 | results = target.edit_features(updates=intersectingPoints) 128 | _report_failures(results) 129 | 130 | return 131 | 132 | 133 | def build_expression(words, match_type, subs): 134 | """Build an all-caps regular expression for matching either exact or 135 | partial strings""" 136 | 137 | re_string = '' 138 | 139 | for word in words: 140 | new_word = '' 141 | for char in word.upper(): 142 | 143 | # If listed, include substitution characters 144 | if char in subs.keys(): 145 | new_word += "[" + char + subs[char] + "]" 146 | 147 | else: 148 | new_word += "[" + char + "]" 149 | 150 | # Filter using only exact matches of the string 151 | if match_type == 'EXACT': 152 | re_string += '\\b{}\\b|'.format(new_word) 153 | 154 | # Filter using all occurances of the letter combinations specified 155 | else: 156 | re_string += '.*{}.*|'.format(new_word) 157 | 158 | # Last character will always be | and must be dropped 159 | return re_string[:-1] 160 | 161 | 162 | def moderate_features(lyr, settings): 163 | rows = _get_features(lyr, settings['sql']) 164 | for row in rows: 165 | for field in settings['scan fields'].split(';'): 166 | try: 167 | text = row.get_value(field) 168 | text = text.upper() 169 | except AttributeError: # Handles empty fields 170 | continue 171 | 172 | if re.search(modlists[settings['list']], text): 173 | row.attributes[settings['field']] = settings['value'] 174 | break 175 | 176 | if rows: 177 | results = lyr.edit_features(updates=rows) 178 | _report_failures(results) 179 | return 180 | 181 | 182 | def _get_value(row, fields, sub): 183 | val = row.attributes[sub] 184 | 185 | if val is None: 186 | val = '' 187 | elif type(val) != str: 188 | for field in fields: 189 | if field['name'] == sub and 'Date' in field['type']: 190 | try: 191 | val = dt.fromtimestamp( 192 | row.attributes[sub]).strftime('%c') 193 | except OSError: # timestamp in milliseconds 194 | val = dt.fromtimestamp( 195 | row.attributes[sub] / 1000).strftime('%c') 196 | break 197 | else: 198 | val = str(val) 199 | return val 200 | 201 | 202 | def build_email(row, fields, settings): 203 | 204 | email_subject = '' 205 | email_body = '' 206 | 207 | if settings['recipient'] in row.fields: 208 | email = row.attributes[settings['recipient']] 209 | else: 210 | email = settings['recipient'] 211 | 212 | try: 213 | html = path.join(path.dirname(__file__), settings['template']) 214 | with open(html) as file: 215 | email_body = file.read() 216 | email_subject = settings['subject'] 217 | if substitutions: 218 | for sub in substitutions: 219 | if sub[1] in row.fields: 220 | val = _get_value(row, fields, sub[1]) 221 | 222 | email_body = email_body.replace(sub[0], val) 223 | email_subject = email_subject.replace(sub[0], val) 224 | else: 225 | email_body = email_body.replace(sub[0], str(sub[1])) 226 | email_subject = email_subject.replace(sub[0], str(sub[1])) 227 | except: 228 | _add_message('Failed to read email template {}'.format(html)) 229 | 230 | return email, email_subject, email_body 231 | 232 | 233 | def main(configuration_file): 234 | 235 | try: 236 | with open(configuration_file) as configfile: 237 | cfg = json.load(configfile) 238 | 239 | gis = GIS(cfg['organization url'], cfg['username'], cfg['password']) 240 | 241 | # Get general id settings 242 | global id_settings 243 | id_settings = {} 244 | for option in cfg['id sequences']: 245 | id_settings[option['name']] = {'interval': int(option['interval']), 246 | 'next value': int(option['next value']), 247 | 'pattern': option['pattern']} 248 | 249 | # Get general moderation settings 250 | global modlists 251 | modlists = {} 252 | subs = cfg['moderation settings']['substitutions'] 253 | for modlist in cfg['moderation settings']['lists']: 254 | words = [str(word).upper().strip() for word in modlist['words'].split(',')] 255 | modlists[modlist['filter name']] = build_expression(words, modlist['filter type'], subs) 256 | 257 | # Get general email settings 258 | server = cfg['email settings']['smtp server'] 259 | username = cfg['email settings']['smtp username'] 260 | password = cfg['email settings']['smtp password'] 261 | tls = cfg['email settings']['use tls'] 262 | from_address = cfg['email settings']['from address'] 263 | if not from_address: 264 | from_address = '' 265 | reply_to = cfg['email settings']['reply to'] 266 | if not reply_to: 267 | reply_to = '' 268 | global substitutions 269 | substitutions = cfg['email settings']['substitutions'] 270 | 271 | # Process each service 272 | for service in cfg['services']: 273 | try: 274 | lyr = FeatureLayer(service['url'], gis=gis) 275 | 276 | # GENERATE IDENTIFIERS 277 | idseq = service['id sequence'] 278 | idfld = service['id field'] 279 | if id_settings and idseq and idfld: 280 | if idseq in id_settings: 281 | new_sequence_value = add_identifiers(lyr, idseq, idfld) 282 | id_settings[idseq]['next value'] = new_sequence_value 283 | else: 284 | _add_message('Sequence {} not found in sequence settings'.format(idseq), 'WARNING') 285 | 286 | # ENRICH REPORTS 287 | if service['enrichment']: 288 | # reversed, sorted list of enrichment settings 289 | enrich_settings = sorted(service['enrichment'], key=lambda k: k['priority'])#, reverse=True) 290 | for reflayer in enrich_settings: 291 | source_features = FeatureLayer(reflayer['url'], gis) 292 | enrich_layer(source_features, lyr, reflayer) 293 | 294 | # MODERATION 295 | if modlists: 296 | for query in service['moderation']: 297 | if query['list'] in modlists: 298 | moderate_features(lyr, query) 299 | else: 300 | _add_message('Moderation list {} not found in moderation settings'.format(modlist), 'WARNING') 301 | 302 | # SEND EMAILS 303 | if service['email']: 304 | with EmailServer(server, username, password, tls) as email_server: 305 | for message in service['email']: 306 | rows = _get_features(lyr, message['sql']) 307 | 308 | for row in rows: 309 | address, subject, body = build_email(row, lyr.properties.fields, message) 310 | if address and subject and body: 311 | 312 | try: 313 | email_server.send(from_address=from_address, 314 | reply_to=reply_to, 315 | to_addresses=[address], 316 | subject=subject, 317 | email_body=body) 318 | 319 | row.attributes[message['field']] = message['sent value'] 320 | except: 321 | _add_message('email failed to send for feature {} in layer {}'.format(row.attributes, service['url'])) 322 | 323 | if rows: 324 | results = lyr.edit_features(updates=rows) 325 | _report_failures(results) 326 | 327 | except Exception as ex: 328 | _add_message('Failed to process service {}\n{}'.format(service['url'], ex)) 329 | 330 | except Exception as ex: 331 | _add_message('Failed. Please verify all configuration values\n{}'.format(ex)) 332 | 333 | finally: 334 | new_sequences = [{'name': seq, 335 | 'interval': id_settings[seq]['interval'], 336 | 'next value': id_settings[seq]['next value'], 337 | 'pattern': id_settings[seq]['pattern']} for seq in id_settings] 338 | 339 | if not new_sequences == cfg['id sequences']: 340 | cfg['id sequences'] = new_sequences 341 | try: 342 | with open(configuration_file, 'w') as configfile: 343 | json.dump(cfg, configfile) 344 | 345 | except Exception as ex: 346 | _add_message('Failed to save identifier configuration values.\n{}\nOld values:{}\nNew values:{}'.format(ex, cfg['id sequences'], new_sequences)) 347 | 348 | if __name__ == '__main__': 349 | main(path.join(path.dirname(__file__), 'servicefunctions.json')) 350 | -------------------------------------------------------------------------------- /user_email_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Thank you for reporting this issue, we'll be in touch soon.

5 | 6 | --------------------------------------------------------------------------------