\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 '@&' in the substitutions space will test for the letter 'a' in place of any @ or & 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 '@&' in the substitutions space will test for the letter 'a' in place of any @ or & 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 |
--------------------------------------------------------------------------------