├── README.md ├── assets ├── img │ ├── Untitled.ico │ ├── avataaars.svg │ ├── digital_skills_logo.png │ ├── explore.JPG │ ├── favicon.ico │ └── portfolio │ │ ├── 70909550_1448576595292384_8858955125624930304_o.jpg │ │ ├── ales-nesetril-ex_p4AaBxbs-unsplash.jpg │ │ ├── altumcode-oZ61KFUQsus-unsplash.jpg │ │ ├── cabin.png │ │ ├── cake.png │ │ ├── chris-ried-ieic5Tq8YMk-unsplash.jpg │ │ ├── christopher-robin-ebbinghaus-pgSkeh0yl8o-unsplash.jpg │ │ ├── circus.png │ │ ├── daan-stevens-yGUuMIqjIrU-unsplash.jpg │ │ ├── game.png │ │ ├── gertruda-valaseviciute-xMObPS6V_gY-unsplash.jpg │ │ ├── halgatewood-com-OgvqXGL7XO4-unsplash.jpg │ │ ├── klaudia-piaskowska-g55bG1O5Lf0-unsplash.jpg │ │ ├── michael-dziedzic-aQYgUYwnCsM-unsplash.jpg │ │ ├── oskar-yildiz-cOkpTiJMGzA-unsplash.jpg │ │ ├── safe.png │ │ ├── sebastian-pichler-oqFHLfLFtmc-unsplash.jpg │ │ ├── steve-johnson-5MTf9XyVVgM-unsplash.jpg │ │ ├── submarine.png │ │ ├── thisisengineering-raeng-hoivM01c-vg-unsplash.jpg │ │ └── tr-n-toan-RdV3_mu0EQU-unsplash.jpg └── mail │ ├── contact_me.js │ └── jqBootstrapValidation.js ├── css └── styles.css ├── index.html ├── js └── scripts.js └── student_solution_files ├── Example_AWS_Comprehend_Key_Phrases_Output.txt ├── Example_AWS_Comprehend_Sentiment_Output.txt ├── aggregated_lambda_function.py ├── basic_lambda_data_decoding.py ├── email_responses.py ├── find_key_phrases.py ├── find_maximum_sentiment.py ├── predict_detail_submission_template.csv ├── send_emails_with_ses.py └── write_data_to_dynamodb.py /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Computing Predict 2 | 3 | #### EXPLORE Data Science Academy Cloud Computing Predict 4 | ## Overview 5 | 6 | In this predict you will make use of Python and AWS to create an intelligent data science portfolio website. At a high-level, the website will be hosted in a serverless manner on AWS; showcasing your amazing data science, machine learning and AI projects. 7 | 8 |

9 | 10 |

11 | 12 | This predict will not only teach you the valuable skill of setting up and consuming AWS services to host a website, but it will also teach you how to use these services in creating an intelligent Natural Language Processing (NLP) service. This NLP solution will allow you to automatically populate and send intelligent emails to interested parties based on the messages they send to you through the website. For example, if a potential recruiter sees a particular portfolio project on your website that interests them and contacts you regarding the said project, it is possible to set up your NLP component to pick up what the tone and key phrases are in the recruiter's message. Some smart programming techniques can then be used to automatically send a response. 13 | 14 |

15 | 16 |
17 | Figure 1: Cloud Computing Predict System Overview 18 |

19 | 20 | 21 | In **Figure 1** the solutions architecture of this predict is depicted. Below follows a brief description of each module in the system: 22 | 23 | >- [x] **[GitHub Template Repo:](https://github.com/)** This dedicated EXPLORE template repo which houses all the content and instructions for a student to complete the Predict. 24 | > 25 | >- [x] **[AWS Lambda:](https://aws.amazon.com/lambda/)** A serverless compute instance responsible for multiple processing steps: 26 | > - Stores the enquiry details within an AWS DynamoDB instance for later retrieval. 27 | > - Forwards the enquiry contents to AWS Comprehend to help formulate an intelligent response to the site visitor. 28 | > - Provides logic to formulate an intelligent response based on AWS Comprehend output. 29 | > - Upon successful completion of these tasks, invokes AWS SES to send emails to the website enquirer and an automated marking email address hosted by EDSA. 30 | > 31 | >- [x] **[AWS Amplify:](https://aws.amazon.com/amplify/)** Responsible for serving the static web content hosted in GitHub which becomes the basis of the student web page. 32 | > 33 | >- [x] **[Amazon DynamoDB:](https://aws.amazon.com/dynamodb/)** A NoSQL database responsible for storing enquiry details from individuals visiting the student webpage. 34 | > 35 | >- [x] **[Anazon SES:](https://aws.amazon.com/ses/)** A code-driven email service responsible for returning an intelligent response to webpage visitors based upon their enquiries. 36 | > 37 | >- [x] **[AWS API Gateway:](https://aws.amazon.com/api-gateway/)** AWS service responsible for receiving enquiry details via an API call from the student webpage, and for passing these on to the internal lambda function. 38 | > 39 | >- [x] **[AWS Comprehend:](https://aws.amazon.com/comprehend/)** An intelligent NLP service capable of characterising sentiment and extracting key-phrases from the ingested text. Used to detect topics within the received webpage enquiries. 40 | 41 | ## Predict Instructions 42 | 43 | The completion of the predict involves nine distinct steps which follow on from one another sequentially. This means that you have to completely finish a particular step before you can move on to the next one. 44 | 45 | 46 | Brief description of each step in the 9-step predict process: 47 | 48 | [Step 1:](#1_section_id) In the first step you will create a **private** fork of this repo (EDSA Cloud-Computing template repo) that stores all of the code needed to host your static website. 49 | 50 | [Step 2:](#2_section_id) This step is all about customising the static website to suit your needs. You will use the provided bootstrap template and general guides to modify the look and contents of the website to fit your preferences. 51 | 52 | [Step 3:](#3_section_id) In the third step you will use AWS Amplify to serve your modified website. We provide the initial steps to begin this process, and then leave the remainder of the task as an exercise for you to understand and complete. 53 | 54 | [Step 4:](#4_section_id) This step involves the creation of an AWS DynamoDB NoSQL database. This database will be used to store website data as and when visitors send an enquiry. 55 | 56 | [Step 5:](#5_section_id) Here you will create an IAM role that will give your AWS Lambda function (created in step 6) the required permissions to interact with AWS Comprehend, AWS SES, AWS DynamoDB and AWS API Gateway. 57 | 58 | [Step 6:](#6_section_id) In this step things get interesting. We set up the AWS Lambda function, a Numpy ARN layer, and an AWS API Gateway trigger: 59 | 60 | - **The AWS Lambda Function** will be used to: 61 | - Write data to Amazon DynamoDB; 62 | - Generate intelligent email responses with Amazon Comprehend, and 63 | - Send emails with Amazon SES. 64 | 65 | - **Numpy ARN:** 66 | - AWS Lambda runs Python on a Linux operating system. This means if you want to use popular libraries such as Numpy in your lambda function, you need to configure your Linux environment accordingly. You can do this by adding layers to your lambda function. In the case of Numpy, you will be adding the relevant layer to your Linux environment. 67 | 68 | - **The AWS API Gateway trigger** configures a publicly accessible HTTP API which listens for POST requests from the webpage. When a request is received, its content is parsed and used to invoke the connected lambda function. 69 | 70 | [Step 7:](#7_section_id) In step seven you will need to configure the Lambda function created previously to write incoming data from the website to the DynamoDB database created in step four. 71 | 72 | [Step 8:](#8_section_id) This step involves the configuration of the AWS SES service so that your pipeline can send emails automatically with the help of a Lambda function. 73 | 74 | [Step 9:](#9_section_id) In this final step you will be required to complete the NLP portion of the predict with the use of AWS Comprehend and by defining additional program logic. At a high level, AWS Comprehend will be used to extract sentiment and key phrases from a message sent from your static website. Using programming logic, you will then define several helper methods and functions which will enable the population of an automated response if the extracted key phrases and sentiment align to specific operating conditions. 75 | 76 | --- 77 | ### 1) Fork the Template Repository 78 | --- 79 | 80 | This repository acts as a resource containing all the requisite files and instructions that you need to successfully complete this predict. To continue development and before you can move on to the following steps, you need to create a private fork of this repo using your own GitHub profile. 81 | 82 | If you have any trouble forking the repo, you might find [this link](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/duplicating-a-repository) helpful. 83 | 84 | | :zap: WARNING :zap: | 85 | | :-------------------- | 86 | | This predict represents an individual project. As such when forking this repo, you need to ensure that it is hosted *privately* within your GitHub account, free from collaboration form your peers or access from the public.| 87 | 88 | --- 89 | ### 2) Modify the Portfolio Webpage Template 90 | --- 91 | 92 | Step two in the process is where you will create your website and add your relevant data science and machine learning portfolio projects. For this step we will provide you with a baseline template which you can change freely in certain areas. 93 | 94 | Below follows a brief description of the files in the bootstrap template: 95 | 96 | | Folder | File Name | Description | 97 | | ---------------- | ---------------- | ---------------- | 98 | | Assets/mail | contact_me.js | Contains all the code required to send data from the filled website form through the defined endpoint to the AWS Lambda function | 99 | | Assets/mail | jqBootstrapValidation.js | This JavaScript file is used to check if data entered in the contact form on the website is in the correct format. If the data is in the expected format, the script will attempt to send this to a specified URL endpoint. If the data is in an incorrect format, an error message will be displayed prompting the user to input the correct information. | 100 | | Assets/img | n/a | This folder contains all the images used within the website. | 101 | | css | style.css | This CSS (Cascading Style Sheets) file is responsible for controlling the style i.e. look and feel of the website | 102 | | js | scripts.js | Various scripts to increase the responsiveness and utility of the website. | 103 | | Project root | index.html | This file defines your static web page, and will be worked on when personalising the website to meet your preferences. | 104 | 105 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 106 | | :-------------------- | 107 | | In this section of the predict you are tasked with **modifying the static website template so that it represents your unique blend of technical skills**. Below you will find some helpful tips that might come handy during this website design/modification journey. | 108 | 109 | **Steps to modify the provided bootstrap template to make the static website your own:** 110 | 111 | I. Open the `index.html` file with an editor of your choice i.e. Pycharm, Brackets, VS Code, etc. and customise the website according to your preference. You can find some general guidance of what to change by reading the code comments we have placed for you. Here is an example comment that you can expect to find in the `index.html` file: 112 | 113 | ```html 114 | 115 | 116 | 117 | ``` 118 | 119 | | :zap: WARNING :zap: | 120 | | :-------------------- | 121 | | While you are free to modify many aspects of the webpage, you should **NOT** alter its functioning. Do not add/remove any fields from the form section, nor modify the variable names captured by the `contact_me.js` script | 122 | 123 | II. Once you've modified the bootstrap template according to your preference, you can move to the next step. Here you will get your hands dirty serving the static website with the help of AWS Amplify :) 124 | 125 | 126 | --- 127 | ### 3) Serve Static Web Page on AWS Amplify 128 | --- 129 | 130 | To serve your static website, you will make use of the AWS Amplify service. As mentioned before, AWS Amplify simplifies the process of web development by providing a serverless framework which removes the need to worry about a running webserver or underlying hosting infrastructure. 131 | 132 | 133 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 134 | | :-------------------- | 135 | | In this section of the predict you again get the chance to showcase your cloud computing skills. You are tasked with using AWS Amplify to serve your modified website in a serverless manner. 136 | 137 | Here it is important to **serve your website with AWS Amplify via your GitHub repository**. That way, when you make any changes to the files sitting in the `deployed` branch, AWS Amplify will automatically pick up the branch changes and re-deploy your website. | 138 | 139 | 140 | | :information_source: NOTE :information_source: | 141 | | :-------------------- | 142 | | By this point in the process your modified website should be running through the AWS Amplify service with a sample domain name: `branch_name.reference_number.amplifyapp.com`. You can view an example of the fully functioning website deployed with AWS Amplify [here](https://main.dajjrrhheaglb.amplifyapp.com/)| 143 | 144 | 145 | --- 146 | ### 4) Create DynamoDb Database 147 | --- 148 | 149 | 150 | Within the proposed system, a database is required that will store all the data sent from the serverless hosted website. For this predict, you will be using the AWS DynamoDB NoSQL database for this purpose. 151 | 152 | The data sent from the website will have the following schema: 153 | 154 | > | Variable Name | Variable Data Type | VariableDescription | 155 | > | ---------------- | ---------------- | ---------------- | 156 | > | Name | String | Name of the person filling out the form on the website | 157 | > | | | | 158 | > | Email Address | String | Email address of the person filling out the form on the website | 159 | > | | | | 160 | > | Phone Number | Integer | Phone number of the person filling out the form on the website | 161 | > | | | | 162 | > | Message | String | Message to you from the person filling out the website form | 163 | > | | | | 164 | 165 | 166 | We will create an additional parameter called `ResponsesID`. 167 | 168 | > | Variable Name | Variable Data Type | VariableDescription | 169 | > | ---------------- | ---------------- | ---------------- | 170 | > | ResponsesID | Integer | Primary key used to identify reponse instance | 171 | 172 | The following steps will help you set this service up within the AWS ecosystem: 173 | 174 | I. Navigate to the AWS DynamoDb service via the AWS Management Console 175 | 176 | II. On the DynamoDB `Dashboard` select `Create Table`. 177 | 178 |

179 | 180 |
181 | Figure 2: Create a DynamoDB table 182 |

183 | 184 | III. Give your table a relevant name, for example, `my-portfolio-data-table`. Store this name for use in later stages of the predict. 185 | 186 | IV. In the `Primary Key` field insert `ResponsesID` and set the type to number. 187 | 188 |

189 | 190 |
191 | Figure 3: Create a DynamoDB table 192 |

193 | 194 | V. Click on `Create`. 195 | 196 | VI. Select the table you just created from the side panel, and under the `Items` tab, click on `Create Item`. 197 | 198 |

199 | 200 |
201 | Figure 4: Add new items and their data types to created table 202 |

203 | 204 | VII. Set an initial index by entering the number '100' in the `ResponsesID` field. (Note that this field represents a unique primary key that will be generated within the Lambda function upon its execution) 205 | 206 | VIII. Click on the `+` icon and select `insert - number`. Name this item `Cell`. Enter `0123456789` in the Value field. 207 | 208 | IX. Click on the `+` icon and select `insert - string`. Name this item `Email`. Enter `student@explore-ai.net` in the Value field. 209 | 210 | X. Click on the `+` icon and select `insert - string`. Name this item `Message`. Enter `Empty Message` in the Value field. 211 | 212 | XI. Click on the `+` icon and select `insert - string`. Name this item `Name`. Enter `Student` in the value field. 213 | 214 |

215 | 216 |
217 | Figure 5: Final populated table after following steps VIII - XI. 218 |

219 | 220 | XII. Store the table entry by clicking on `Save`. 221 | 222 | XIII. Select the created table and navigate to the `Overview` tab. 223 | 224 | XIV. Scroll down and note your Amazon Resource Name (ARN) for the created table. *Save this for later use in the IAM policy creation steps*. 225 | 226 | --- 227 | ### 5) Create an IAM Role 228 | --- 229 | 230 | In step 5 you once again get the chance to show off your cloud computing skills. This predict involves using *a lot* of AWS services, and as such we need a comprehensive IAM Role that will adequately govern the access and authority of the AWS Lambda function. In this predict you will make use of the following services: 231 | 232 | - AWS Lambda 233 | 234 | - AWS API Gateway 235 | 236 | - AWS Comprehend 237 | 238 | - AWS SES 239 | 240 | - AWS DynamoDB 241 | 242 | 243 | You therefore need an IAM role to give the Lambda function the required access to the various services that we will be using in this predict. In total, you need to create four policies for this IAM Role: 244 | 245 | | **Policy** | **Description** | 246 | |------------|-------------| 247 | |**AWS Comprehend Policy** | Allows the Lambda function to call AWS Comprehend in order calculate a sentiment score and extract key phrases from received text entered on the website. | 248 | |**AWS SES Policy** | Allows the Lambda function to invoke the AWS SES service in order to send automated responses via email. | 249 | |**AWS Basic Lambda Execution Policy** | Grants the Lambda function permission to access AWS services and resources. | 250 | |**AWS DynamoDB Policy** | Gives the Lambda function write permissions in order to store data from the website to a designated (existing) AWS DynamoDB table. | 251 | 252 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 253 | | :-------------------- | 254 | | You are tasked with **using the AWS IAM service to set up the necessary policies as described above**. It is important to remember that the DynamoDB policy will be of type inline, and that you will need to use the specific DynamoDB table ARN associated with the table you created in [Step 4](#4_section_id) to appropriately set up this policy.| 255 | 256 | 257 | --- 258 | ### 6) Set-up the Initial AWS Lambda Function and the AWS API Gateway Trigger 259 | --- 260 | 261 | In this step you will set up the AWS API Gateway and the initial AWS Lambda function. This will be a three-part process: 262 | 263 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 264 | | :-------------------- | 265 | | Below is a brief description of the required three part process. You are, however, tasked with executing the three parts and little-to-no additional guidance will be give here. This is because, setting up an AWS Lambdas function is a key outcome of this course and should be something you are very familiar with by this time.| 266 | 267 | 268 | #### Part 1: Lambda Initialisation 269 | 270 | Part 1 involves creating the AWS Lambda Function with a Python 3.7 runtime. You will also attach the IAM role created within step 5 to govern the lambda function's access control. 271 | 272 | 273 | #### Part 2: Layer Addition 274 | 275 | In part 2, the objective is to add an ARN layer to the generated lambda function - one for Numpy. You need to add this layer so that you may use the Numpy library building your solution i.e. the Python code telling the lambdas function what to do. Remember these layers are `region` and `runtime` specific. Find your relevant ARN [here](https://github.com/keithrozario/Klayers). 276 | 277 | #### Part 3: API Gateway Creation 278 | 279 | The final part within step 6 is to create the AWS API Gateway and set this endpoint as your lambda's function trigger. At a high level, this gateway is responsible for interfacing between your deployed website (running from AWS Amplify) and the lambda function you have created - invoking the lambda when the gateway receives an API call (HTTP POST request sent by the webpage) and passing through its data payload to the function. This process sets off a chain of programmatic events which ultimately will generate and send your intelligent email automatically. 280 | 281 | Your API should have the following characteristics: 282 | - **API Type**: HTTP. 283 | - **Supported Method**: POST. 284 | - **Utilised Authorization**: None. 285 | - **[Cross-origin resource sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)**: Enabled. 286 | - **Detailed billing**: Disabled. 287 | 288 | Once you have created the API Gateway, you should replace the URL of the endpoint in your `contact_me.js` file to your specific endpoint URL. The following steps might help you in your endeavour. 289 | 290 | 291 | I. Note the `API endpoint` address under the configured trigger. 292 | 293 | II. Use this API and navigate to the `contact_me.js` file in your GitHub repo created in step 1. 294 | 295 |

296 | 297 |
298 | Figure 6: Created API Gateway 299 |

300 | 301 | III. Replace the existing API URL with your API endpoint. 302 | 303 | IV. Commit the changes and wait for the branch to be redeployed by AWS Amplify. 304 | 305 | 306 | | :information_source: NOTE :information_source: | 307 | | :-------------------- | 308 | | By this point in the process your website should enable you to fill out the form with the appropriate information and, upon submission, you should receive the `Your message has been sent` notification. | 309 | 310 | --- 311 | ### 7) AWS Lambda Function for Writing to DynamoDB 312 | --- 313 | 314 | You are now at a point in the predict where you can start building the actual lambda functionality. Initially you will start off with the simple task of using the AWS Lambda + API Gateway to write the data coming from the website form to the previously created DynamoDB database. If needed, you can familiarise yourself with the overall process as represented within **figure 1**. 315 | 316 |

317 | 318 |
319 | Figure 7: Lambda writing data from the website to DynamoDB 320 |

321 | 322 | 323 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 324 | | :-------------------- | 325 | | **Set up the lambda function to write the website POST data to DynamoDB**. To get the functionality displayed in figure 7, you will need to use Python to tell your AWS Lambda function what to do and how to do it. Luckily we have some stater code for you. You can use the code found [here](student_solution_files/basic_lambda_data_decoding.py) to read and decode the incoming data from the website. Then, using the boilerplate code found [here](student_solution_files/write_data_to_dynamodb.py) as starting point, you can enable your lambda function to write to DynamoDB.| 326 | 327 | --- 328 | ### 8) AWS Lambda Function for Sending an Email with AWS SES 329 | --- 330 | 331 | In this step you will setup AWS Simple Email Service (SES), to programmatically send emails when required. This is a two-part process: 332 | 333 | - The first part entails the verification of email addresses for the sending and receiving of messages generated by SES. 334 | 335 | - The second part sees you configure your growing lambda function to send emails to specified addresses when triggered by a POST request being received by the API gateway. 336 | 337 | #### Part 1: Verify your Email with Amazon SES: 338 | 339 | When using Amazon SES for the first time, all accounts start out in sandbox mode. This is to prevent new accounts from abusing the service and sending out spam emails. In sandbox mode the following restrictions apply: 340 | 341 | - You can only send mail to verified email addresses and domains, or to the [Amazon SES mailbox simulator](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-simulator.html). 342 | 343 | - You can only send mail from verified email addresses and domains. 344 | 345 | - You can send a maximum of 200 messages per 24-hour period. 346 | 347 | - You can send a maximum of 1 message per second. 348 | 349 | When your account is promoted out of the sandbox, you can send emails to any recipient, regardless of whether the recipient's address or domain is verified. However, you still have to verify all identities that you use as "From", "Source", "Sender", or "Return-Path" addresses. 350 | 351 | 352 | | :information_source: NOTE :information_source: | 353 | | :-------------------- | 354 | | You do not have to move your account out of Sandbox Mode complete the predict. However, for interest, to move out of sandbox mode you can follow [these](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/request-production-access.html) instructions. | 355 | 356 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 357 | | :-------------------- | 358 | | There are two predict-related tasks for this step. The first of these is the **verification of sender and recipient email addresses**. Here you will need to add `edsa.predicts@explore-ai.net` as a recipient address within SES. This email account forms part of our predict assessment process and will be used at a later stage for predict marking. Note that when requesting verification for this email address, *it may take up to 24 hours for confirmation to be given*. In addition to the explore email address, **you need to configure a sender and a recipient address respectively for your testing purposes**.| 359 | 360 | #### Part 2: Programmatically send an email via Amazon SES: 361 | 362 | Having registered the necessary email addresses, you are now ready to invoke AWS SES within your lambda function to automate the sending of email messages. 363 | 364 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 365 | | :-------------------- | 366 | | At this point we want you to once again show us your great cloud computing and python skills. The **second task involves sending a sample email to your recipient address from your sender address, using AWS SES**. Here we provide some basic guidance in the form of [this](student_solution_files/send_emails_with_ses.py) this template, which your are required to complete and to add to your lambda function in order to help you accomplish your end goal.| 367 | 368 | --- 369 | ### 9) AWS Lambda Function for Using Amazon Comprehend 370 | --- 371 | 372 | In this final step you will be building out the predict's NLP functionality with the help of AWS Comprehend. The NLP functionality will enable you to extract the overwhelming sentiment (categorical variable) from a message, as well as a list of its key phrases as determined by AWS comprehend. With the sentiment information and a list of key phrases, you can build in intelligent, automated email responses into your AWS Lambda function. To help you thoroughly understand this section, we provide a three-part breakdown wherein we describe each key element involved in the formation of an intelligent response. 373 | 374 | The **first part** is the process description; where we go through the logic of how to use AWS comprehend to extract sentiment and key phrases. The **second part** covers the helper functions utilised; where we describe two helper functions and the main lambda function required to orchestrate an intelligent automated response. The third and **final part** is an end-to-end example; where we simulate what should be achieved once the entire AWS Lambda/AWS Comprehend/AWS SES integration is built. 375 | 376 | #### Part 1: Process Description 377 | 378 | At a high-level, the entire intelligent response process can be described as follow: 379 | 380 | 1. The form is filled out on the website and sent. This form contains the following sender data: 381 | - `name` 382 | - `cellphone number` 383 | - `email address`, and 384 | - `message`. 385 | 386 | 2. The data passes through the AWS API Gateway and triggers the AWS Lambda function. 387 | 388 | 3. The sender data is decoded. 389 | 390 | 4. The `message` is used in AWS Comprehend to extract the message sentiment and a dictionary of key phrases. 391 | 392 | 5. A custom helper function finds the score and value of the most likely sentiment present in the message. 393 | 394 | 6. Another helper function is used to convert the list of key phrases, generated by AWS Comprehend, to a numpy array. This array contains individual words present within the key phrase dictionary. This helper function also performs a lookup - producing a set of key phrases which match a custom list of keywords we wish to detect and reply to. 395 | 396 | 7. Having extracted the above data from the received message, the following email logic can be applied: 397 | 398 | ```python 399 | 400 | if message_sentiment == 'Desired_Sentiment': 401 | 402 | if (AWS_Comprehend_Extracted_Words == Provided_List_of_Words): 403 | generate some personalised response based on message content 404 | 405 | else: 406 | generate some generic response based on message content 407 | 408 | ``` 409 | 410 | #### Part 2: Helper Function Descriptions 411 | 412 | In this section we provide an overview of the helper functions required to send the intelligent, automated email responses. 413 | 414 | **Function 1 - `find_max_sentiment(Comprehend_Sentiment_Output):`** 415 | 416 | This function extracts and returns the highest probable sentiment from a given message. 417 | - Input argument: 418 | - Comprehend_Sentiment_Output: Sentiment dictionary generated by AWS Comprehend. 419 | - Outputs: 420 | - overall_sentiment: A string representing the most probable sentiment within the message. 421 | - sentiment_score: The value of the highest probable sentiment present in the message. 422 | - The function implementation can be found [here](student_solution_files/find_maximum_sentiment.py) 423 | 424 | **Function 2 - `key_phrase_finder(list_of_important_phrases, list_of_extracted_phrases):`** 425 | 426 | This function attempts to find a match between the words in a key phrases dictionary produced by AWS Comprehend to a provided custom list of words. The result of the search is returned as a boolean variable. 427 | - Input arguments: 428 | - list_of_important_phrases: A string-based list of words representing topics to be detected i.e. `['CV', 'data', 'science']` 429 | - list_of_extracted_phrases: An AWS Comprehend key phrases dictionary. 430 | - Outputs: 431 | - listing: An empty list to append the individual words present in the AWS Comprehend key phrases dictionary. 432 | - phrase_checker: A boolean variable representing the whether a match is discovered between the function's input lists. 433 | - The function implementation can be found [here](student_solution_files/find_key_phrases.py) 434 | 435 | **Function 3 - `email_response(name, email_address, critical_phrase_list, sentiment_list, sentiment_scores, list_of_extracted_phrases, AWS_Comprehend_Sentiment_Dump):`** 436 | 437 | This function takes in the parsed information from the sender i.e. `name`, `email_address`, the `sentiment` and the `key phrases` output from AWS Comprehend, and a `list of critical phrases`, and uses the logic described in the 'Process Description' section to populate a email. 438 | 439 | - Input arguments: 440 | - name: The name of the requester. 441 | - email_address: The email address of the requester. 442 | - critical_phrase_list: A list of words that you want to match to the words in the AWS key phrases dictionary. 443 | - sentiment_list: A list of possible sentiments that a message could contain i.e. ['Positive', 'Negative', 'Neutral', 'Mixed']. 444 | - sentiment_scores: The sentiment score attached to each of the listed sentiments in the `sentiment_list` as determined by AWS Comprehend. 445 | - list_of_extracted_phrases: A list of the individual words present in the key phrases dictionary. 446 | - AWS_Comprehend_Sentiment_Dump: The sentiment summary dictionary as populated by AWS Comprehend. 447 | - Outputs: 448 | - Text: The intelligently populated email response based on the contents of the sender's message. 449 | - - The function implementation can be found [here](student_solution_files/email_responses.py) 450 | 451 | The email_response method works as follow: 452 | 453 | 1. You supply a list of words which correspond to topics which you'd like to extract/filter for within an incoming website enquiry. 454 | 2. The supplied list of words are matched with words extracted from the AWS Comprehend key phrases dictionary. 455 | 3. A `boolean` response is given for each match detected across various configured keyword categories. 456 | 4. Given the match, or potentially multiple matches, you can set up logic to start building an email response sequentially. 457 | 458 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 459 | | :-------------------- | 460 | | The final task of the predict requires you to use the provided functions and the boilerplate AWS Comprehend/AWS Lambda code found [here](student_solution_files/aggregated_lambda_function.py) to build out the full functionality of the automated predict pipeline.| 461 | 462 | 463 | # End-to-end Example 464 | 465 | In this section we will review a basic example of how an email response is generated with the full solution architecture. 466 | 467 | Let's say someone visiting your deployed webpage posts the following sample message using the form: 468 | 469 | > Hi Explore Data Science Student, 470 | > 471 | > I love your website and your portfolio projects. I also had a look at your GitHub page and I am quite impressed with the quality of your work. 472 | > 473 | > Your medium articles also captured my attention. 474 | > 475 | > Could you please forward me your CV so that I can present you for a potential job at one of my clients. It is for a six month contract and I feel that your experience and skills match the job specifications perfectly. 476 | > 477 | > Thank you so much for your time. 478 | > 479 | > Kind regards, 480 | > 481 | > Charlotte Regression 482 | > 483 | > Senior HR Manager, Real Analytics Resources 484 | 485 | Running this message through our AWS Comprehend service produces [these](student_solution_files/Example_AWS_Comprehend_Key_Phrases_Output.txt) key phrases and [this](student_solution_files/Example_AWS_Comprehend_Sentiment_Output.txt) sentiment classification output. 486 | 487 | From this output it can be seen that AWS Comprehend picked up that the message has a positive sentiment, and that it contains key phrases such as: 'your portfolio projects', 'your GitHub page', 'Your medium articles', 'a potential job', etc. We can therefore set up some logic along with our helper functions to build out our responses. 488 | 489 | To define this logic, we start by providing some sample `string` replies that we can add to our email response if certain supplied words match the words in our AWS Comprehend key phrases dictionary. These sample replies can take the following form: 490 | 491 | **1. Sample reply if a CV related word is matched**: 492 | 493 | ``` 494 | CV_Text = 'I see that you mentioned my C.V in your message. I am happy to forward you my C.V in response. If you have any other questions or C.V related queries please do get in touch. ' 495 | ``` 496 | 497 | **2. Sample reply if a portfolio project related word is matched** 498 | 499 | ``` 500 | Project_Text = 'The projects I listed on my site only include those not running in production. I have several other projects that might interest you.' 501 | ``` 502 | **3. Sample reply if a Medium article related word is matched** 503 | 504 | ``` 505 | Article_Text = 'In your message you mentioned my blog posts and data science articles. I have several other articles published in academic journals. Please do let me know if you are interested - I am happy to forward them to you.' 506 | ``` 507 | **4. Sample reply if the detected sentiment is negative** 508 | 509 | ``` 510 | Negative_Text = 'I see that you are unhappy in your response. Can we please set up a session to discuss why you are not happy, be it with the website, my personal projects or anything else. \n\nLooking forward to our discussion. \n\nKind Regards, \n\nMy Name' 511 | ``` 512 | 513 | **5. Sample reply if the detected sentiment is neutral** 514 | 515 | ``` 516 | Neutral_Text = 'Thank you for your email. Let me know if you need any additional information.\n\nKind Regards, \n\nMyName' 517 | ``` 518 | 519 | **6. Sample farewell reply** 520 | 521 | ``` 522 | Farewell_Text = 'Again, Thank you for your email.\n\nIf there is anything else I can assist you with please let me know and I will set up a meeting for us to meet in person.\n\nKind Regards, \n\nMyName' 523 | ``` 524 | 525 | With sample replies now defined, we can use conditional logic and string manipulation techniques to *build* up a response. 526 | 527 | For instance, we define a `Phrase_Matcher_Project` variable that makes use of the `key_phrase_finder` function and passes a list of words that might be project related, for example `['github', 'git', 'Git', 'GitHub', 'projects', 'portfolio', 'Portfolio']`. 528 | 529 | If any of these words now match the extracted keywords in the Comprehend dictionary, the value of the `Phrase_Matcher_Project` variable will be `True`. We can then use conditional logic and string manipulation to add a greeting, the project text and the farewell text to an `email_reply` variable. In the example we've presented, this `email_reply` variable will take on the following form: 530 | 531 | > Good day Charlotte Regression, 532 | > 533 | > The projects I listed on my site only include those not running in production. I have several other projects that might interest you. 534 | > 535 | > Again, thank you for your email. 536 | > 537 | > If there is anything else I can assist you with please let me know and I will set up a meeting for us to meet in person. 538 | > 539 | > Kind Regards, 540 | > 541 | > MyName 542 | 543 | Remember that in the example additional keywords were present such as: `CV`, `projects`, `articles`, etc. We could therefore set up a CV, Project and Article phrase checker and use the same logic as discussed above to generate an intelligent automated response. The `email_response` variable would then take on the following value for the full example: 544 | 545 | > Good day Charlotte Regression, 546 | > 547 | > I see that you mentioned my C.V in your message. I am happy to forward you my C.V in response. If you have any other questions or C.V related queries please do get in touch. 548 | > 549 | > In your message you mentioned my blog posts and data science articles. I have several other articles published in academic journals. Please do let me know if you are interested - I am happy to forward them to you 550 | > 551 | > The projects I listed on my site only include those not running in production. I have several other projects that might interest you. 552 | > 553 | > Again, thank you for your email. 554 | > 555 | > If there is anything else I can assist you with please let me know and I will set up a meeting for us to meet in person. 556 | > 557 | > Kind Regards, 558 | > 559 | > MyName 560 | 561 | | :information_source: NOTE :information_source: | 562 | | :-------------------- | 563 | | The boilerplate lambda function for the full solution can be found [here](student_solution_files/aggregated_lambda_function.py) | 564 | 565 | ## Predict Assessment 566 | 567 | | :triangular_flag_on_post: PREDICT TASK :triangular_flag_on_post: | 568 | | :-------------------- | 569 | | Please ensure that all the below information is submitted in order to be fairly marked on your performance in completing this Predict.| 570 | 571 | Once completed, your predict solution will be marked at a time indicated within the Predict Overview Slides and as communicated on Athena. To facilitate this assessment, please submit the following details via the Predict Submission tab on Athena using [this](student_solution_files/predict_detail_submission_template.csv) simple template: 572 | - Your **Name and Surname**; 573 | - The **Website URL** of your deployed project as recorded in [Step 3](#3_section_id) of this guide, and 574 | - Your **AWS API Gateway URL** as recorded in part 3 of [Step 6](#6_section_id). 575 | 576 | Please also ensure that you have verified the `edsa.predicts@explore-ai.net` email address via AWS SES as instructed in step 8, part 1. 577 | 578 | Note that while you are free to personalise many aspects of this predict, for assessment purposes you will only be awarded marks for its functionality. As such, please make every effort to ensure that your completed predict solution is able to replicate the functioning specified throughout the above instruction steps. 579 | 580 | ## Conclusion 581 | 582 | If you followed the nine-step process described above correctly, you should now have a fully functioning portfolio website capable of sending intelligent email responses in an automated way. Congrats! 583 | 584 | Following the conclusion of this predict and its assessment, you are free (and we encourage you) to tweak various aspects of the project to personalise it even further. Here are just a few ideas you can try: 585 | 586 | - Set up more logic that will cater for a wider array of email responses. 587 | - Use a different [Bootstrap](https://getbootstrap.com/) template and see if you can reproduce the results of the project. 588 | - Integrate more AWS services. For example, see if you can integrate AWS QuickSight to visualize the data in your AWS DynamoDB NoSQL database. Alternatively, you could try to use the [AWS Lex](https://aws.amazon.com/lex/) service to place a chatbot on your profile page, adding another layer of intelligent interaction to engage potential clients. 589 | 590 |

591 | 592 |

593 | -------------------------------------------------------------------------------- /assets/img/Untitled.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/Untitled.ico -------------------------------------------------------------------------------- /assets/img/avataaars.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 38 | Created with getavataaars.com 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 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 106 | 107 | 108 | 110 | 111 | 112 | 113 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 189 | 191 | 192 | 193 | 195 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /assets/img/digital_skills_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/digital_skills_logo.png -------------------------------------------------------------------------------- /assets/img/explore.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/explore.JPG -------------------------------------------------------------------------------- /assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/favicon.ico -------------------------------------------------------------------------------- /assets/img/portfolio/70909550_1448576595292384_8858955125624930304_o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/70909550_1448576595292384_8858955125624930304_o.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/ales-nesetril-ex_p4AaBxbs-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/ales-nesetril-ex_p4AaBxbs-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/altumcode-oZ61KFUQsus-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/altumcode-oZ61KFUQsus-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/cabin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/cabin.png -------------------------------------------------------------------------------- /assets/img/portfolio/cake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/cake.png -------------------------------------------------------------------------------- /assets/img/portfolio/chris-ried-ieic5Tq8YMk-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/chris-ried-ieic5Tq8YMk-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/christopher-robin-ebbinghaus-pgSkeh0yl8o-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/christopher-robin-ebbinghaus-pgSkeh0yl8o-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/circus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/circus.png -------------------------------------------------------------------------------- /assets/img/portfolio/daan-stevens-yGUuMIqjIrU-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/daan-stevens-yGUuMIqjIrU-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/game.png -------------------------------------------------------------------------------- /assets/img/portfolio/gertruda-valaseviciute-xMObPS6V_gY-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/gertruda-valaseviciute-xMObPS6V_gY-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/halgatewood-com-OgvqXGL7XO4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/halgatewood-com-OgvqXGL7XO4-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/klaudia-piaskowska-g55bG1O5Lf0-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/klaudia-piaskowska-g55bG1O5Lf0-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/michael-dziedzic-aQYgUYwnCsM-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/michael-dziedzic-aQYgUYwnCsM-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/oskar-yildiz-cOkpTiJMGzA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/oskar-yildiz-cOkpTiJMGzA-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/safe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/safe.png -------------------------------------------------------------------------------- /assets/img/portfolio/sebastian-pichler-oqFHLfLFtmc-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/sebastian-pichler-oqFHLfLFtmc-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/steve-johnson-5MTf9XyVVgM-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/steve-johnson-5MTf9XyVVgM-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/submarine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/submarine.png -------------------------------------------------------------------------------- /assets/img/portfolio/thisisengineering-raeng-hoivM01c-vg-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/thisisengineering-raeng-hoivM01c-vg-unsplash.jpg -------------------------------------------------------------------------------- /assets/img/portfolio/tr-n-toan-RdV3_mu0EQU-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Explore-AI/cloud-computing-predict/72c72ba949d7401b27052198cbfe14d75f5580e1/assets/img/portfolio/tr-n-toan-RdV3_mu0EQU-unsplash.jpg -------------------------------------------------------------------------------- /assets/mail/contact_me.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $( 3 | "#contactForm input,#contactForm textarea,#contactForm button" 4 | ).jqBootstrapValidation({ 5 | preventSubmit: true, 6 | submitError: function ($form, event, errors) { 7 | // additional error messages or events 8 | }, 9 | submitSuccess: function ($form, event) { 10 | event.preventDefault(); // prevent default submit behaviour 11 | // get values from FORM 12 | console.log('Capturing Data') 13 | var name = $("input#name").val(); 14 | var email = $("input#email").val(); 15 | var phone = $("input#phone").val(); 16 | var message = $("textarea#message").val(); 17 | var firstName = name; // For Success/Failure Message 18 | // Check for white space in name for Success/Fail message 19 | if (firstName.indexOf(" ") >= 0) { 20 | firstName = name.split(" ").slice(0, -1).join(" "); 21 | } 22 | $this = $("#sendMessageButton"); 23 | $this.prop("disabled", true); // Disable submit button until AJAX call is complete to prevent duplicate messages 24 | var json = {name: name, phone: phone, email: email, message: message} 25 | $.ajax({ 26 | // --- CHANGE THIS LINE TO YOUR OWN API GATEWAY -------- 27 | url: "{Insert your own AWS API Gateway Endpoint URL here}", 28 | // ------------------------------------------------------ 29 | type: "POST", 30 | data: JSON.stringify(json), 31 | cache: false, 32 | success: function () { 33 | // Success message 34 | console.log("Success") 35 | $("#success").html("
"); 36 | $("#success > .alert-success") 37 | .html( 38 | ""); 41 | $("#success > .alert-success").append( 42 | "Your message has been sent. " 43 | ); 44 | $("#success > .alert-success").append("
"); 45 | //clear all fields 46 | $("#contactForm").trigger("reset"); 47 | }, 48 | error: function () { 49 | // Fail message 50 | $("#success").html("
"); 51 | $("#success > .alert-danger") 52 | .html( 53 | ""); 56 | $("#success > .alert-danger").append( 57 | $("").text( 58 | "Sorry " + 59 | firstName + 60 | ", it seems that my mail server is not responding. Please try again later!" 61 | ) 62 | ); 63 | $("#success > .alert-danger").append("
"); 64 | //clear all fields 65 | $("#contactForm").trigger("reset"); 66 | }, 67 | complete: function () { 68 | setTimeout(function () { 69 | $this.prop("disabled", false); // Re-enable submit button when AJAX call is complete 70 | }, 1000); 71 | }, 72 | }); 73 | }, 74 | filter: function () { 75 | return $(this).is(":visible"); 76 | }, 77 | }); 78 | 79 | $('a[data-toggle="tab"]').click(function (e) { 80 | e.preventDefault(); 81 | $(this).tab("show"); 82 | }); 83 | }); 84 | 85 | /*When clicking on Full hide fail/success boxes */ 86 | $("#name").focus(function () { 87 | $("#success").html(""); 88 | }); 89 | -------------------------------------------------------------------------------- /assets/mail/jqBootstrapValidation.js: -------------------------------------------------------------------------------- 1 | /* jqBootstrapValidation 2 | * A plugin for automating validation on Twitter Bootstrap formatted forms. 3 | * 4 | * v1.3.6 5 | * 6 | * License: MIT - see LICENSE file 7 | * 8 | * http://ReactiveRaven.github.com/jqBootstrapValidation/ 9 | */ 10 | 11 | (function( $ ){ 12 | 13 | var createdElements = []; 14 | 15 | var defaults = { 16 | options: { 17 | prependExistingHelpBlock: false, 18 | sniffHtml: true, // sniff for 'required', 'maxlength', etc 19 | preventSubmit: true, // stop the form submit event from firing if validation fails 20 | submitError: false, // function called if there is an error when trying to submit 21 | submitSuccess: false, // function called just before a successful submit event is sent to the server 22 | semanticallyStrict: false, // set to true to tidy up generated HTML output 23 | autoAdd: { 24 | helpBlocks: true 25 | }, 26 | filter: function () { 27 | // return $(this).is(":visible"); // only validate elements you can see 28 | return true; // validate everything 29 | } 30 | }, 31 | methods: { 32 | init : function( options ) { 33 | 34 | var settings = $.extend(true, {}, defaults); 35 | 36 | settings.options = $.extend(true, settings.options, options); 37 | 38 | var $siblingElements = this; 39 | 40 | var uniqueForms = $.unique( 41 | $siblingElements.map( function () { 42 | return $(this).parents("form")[0]; 43 | }).toArray() 44 | ); 45 | 46 | $(uniqueForms).bind("submit", function (e) { 47 | var $form = $(this); 48 | var warningsFound = 0; 49 | var $inputs = $form.find("input,textarea,select").not("[type=submit],[type=image]").filter(settings.options.filter); 50 | $inputs.trigger("submit.validation").trigger("validationLostFocus.validation"); 51 | 52 | $inputs.each(function (i, el) { 53 | var $this = $(el), 54 | $controlGroup = $this.parents(".control-group").first(); 55 | if ( 56 | $controlGroup.hasClass("warning") 57 | ) { 58 | $controlGroup.removeClass("warning").addClass("error"); 59 | warningsFound++; 60 | } 61 | }); 62 | 63 | $inputs.trigger("validationLostFocus.validation"); 64 | 65 | if (warningsFound) { 66 | if (settings.options.preventSubmit) { 67 | e.preventDefault(); 68 | } 69 | $form.addClass("error"); 70 | if ($.isFunction(settings.options.submitError)) { 71 | settings.options.submitError($form, e, $inputs.jqBootstrapValidation("collectErrors", true)); 72 | } 73 | } else { 74 | $form.removeClass("error"); 75 | if ($.isFunction(settings.options.submitSuccess)) { 76 | settings.options.submitSuccess($form, e); 77 | } 78 | } 79 | }); 80 | 81 | return this.each(function(){ 82 | 83 | // Get references to everything we're interested in 84 | var $this = $(this), 85 | $controlGroup = $this.parents(".control-group").first(), 86 | $helpBlock = $controlGroup.find(".help-block").first(), 87 | $form = $this.parents("form").first(), 88 | validatorNames = []; 89 | 90 | // create message container if not exists 91 | if (!$helpBlock.length && settings.options.autoAdd && settings.options.autoAdd.helpBlocks) { 92 | $helpBlock = $('
'); 93 | $controlGroup.find('.controls').append($helpBlock); 94 | createdElements.push($helpBlock[0]); 95 | } 96 | 97 | // ============================================================= 98 | // SNIFF HTML FOR VALIDATORS 99 | // ============================================================= 100 | 101 | // *snort sniff snuffle* 102 | 103 | if (settings.options.sniffHtml) { 104 | var message = ""; 105 | // --------------------------------------------------------- 106 | // PATTERN 107 | // --------------------------------------------------------- 108 | if ($this.attr("pattern") !== undefined) { 109 | message = "Not in the expected format"; 110 | if ($this.data("validationPatternMessage")) { 111 | message = $this.data("validationPatternMessage"); 112 | } 113 | $this.data("validationPatternMessage", message); 114 | $this.data("validationPatternRegex", $this.attr("pattern")); 115 | } 116 | // --------------------------------------------------------- 117 | // MAX 118 | // --------------------------------------------------------- 119 | if ($this.attr("max") !== undefined || $this.attr("aria-valuemax") !== undefined) { 120 | var max = ($this.attr("max") !== undefined ? $this.attr("max") : $this.attr("aria-valuemax")); 121 | message = "Too high: Maximum of '" + max + "'"; 122 | if ($this.data("validationMaxMessage")) { 123 | message = $this.data("validationMaxMessage"); 124 | } 125 | $this.data("validationMaxMessage", message); 126 | $this.data("validationMaxMax", max); 127 | } 128 | // --------------------------------------------------------- 129 | // MIN 130 | // --------------------------------------------------------- 131 | if ($this.attr("min") !== undefined || $this.attr("aria-valuemin") !== undefined) { 132 | var min = ($this.attr("min") !== undefined ? $this.attr("min") : $this.attr("aria-valuemin")); 133 | message = "Too low: Minimum of '" + min + "'"; 134 | if ($this.data("validationMinMessage")) { 135 | message = $this.data("validationMinMessage"); 136 | } 137 | $this.data("validationMinMessage", message); 138 | $this.data("validationMinMin", min); 139 | } 140 | // --------------------------------------------------------- 141 | // MAXLENGTH 142 | // --------------------------------------------------------- 143 | if ($this.attr("maxlength") !== undefined) { 144 | message = "Too long: Maximum of '" + $this.attr("maxlength") + "' characters"; 145 | if ($this.data("validationMaxlengthMessage")) { 146 | message = $this.data("validationMaxlengthMessage"); 147 | } 148 | $this.data("validationMaxlengthMessage", message); 149 | $this.data("validationMaxlengthMaxlength", $this.attr("maxlength")); 150 | } 151 | // --------------------------------------------------------- 152 | // MINLENGTH 153 | // --------------------------------------------------------- 154 | if ($this.attr("minlength") !== undefined) { 155 | message = "Too short: Minimum of '" + $this.attr("minlength") + "' characters"; 156 | if ($this.data("validationMinlengthMessage")) { 157 | message = $this.data("validationMinlengthMessage"); 158 | } 159 | $this.data("validationMinlengthMessage", message); 160 | $this.data("validationMinlengthMinlength", $this.attr("minlength")); 161 | } 162 | // --------------------------------------------------------- 163 | // REQUIRED 164 | // --------------------------------------------------------- 165 | if ($this.attr("required") !== undefined || $this.attr("aria-required") !== undefined) { 166 | message = settings.builtInValidators.required.message; 167 | if ($this.data("validationRequiredMessage")) { 168 | message = $this.data("validationRequiredMessage"); 169 | } 170 | $this.data("validationRequiredMessage", message); 171 | } 172 | // --------------------------------------------------------- 173 | // NUMBER 174 | // --------------------------------------------------------- 175 | if ($this.attr("type") !== undefined && $this.attr("type").toLowerCase() === "number") { 176 | message = settings.builtInValidators.number.message; 177 | if ($this.data("validationNumberMessage")) { 178 | message = $this.data("validationNumberMessage"); 179 | } 180 | $this.data("validationNumberMessage", message); 181 | } 182 | // --------------------------------------------------------- 183 | // EMAIL 184 | // --------------------------------------------------------- 185 | if ($this.attr("type") !== undefined && $this.attr("type").toLowerCase() === "email") { 186 | message = "Not a valid email address"; 187 | if ($this.data("validationValidemailMessage")) { 188 | message = $this.data("validationValidemailMessage"); 189 | } else if ($this.data("validationEmailMessage")) { 190 | message = $this.data("validationEmailMessage"); 191 | } 192 | $this.data("validationValidemailMessage", message); 193 | } 194 | // --------------------------------------------------------- 195 | // MINCHECKED 196 | // --------------------------------------------------------- 197 | if ($this.attr("minchecked") !== undefined) { 198 | message = "Not enough options checked; Minimum of '" + $this.attr("minchecked") + "' required"; 199 | if ($this.data("validationMincheckedMessage")) { 200 | message = $this.data("validationMincheckedMessage"); 201 | } 202 | $this.data("validationMincheckedMessage", message); 203 | $this.data("validationMincheckedMinchecked", $this.attr("minchecked")); 204 | } 205 | // --------------------------------------------------------- 206 | // MAXCHECKED 207 | // --------------------------------------------------------- 208 | if ($this.attr("maxchecked") !== undefined) { 209 | message = "Too many options checked; Maximum of '" + $this.attr("maxchecked") + "' required"; 210 | if ($this.data("validationMaxcheckedMessage")) { 211 | message = $this.data("validationMaxcheckedMessage"); 212 | } 213 | $this.data("validationMaxcheckedMessage", message); 214 | $this.data("validationMaxcheckedMaxchecked", $this.attr("maxchecked")); 215 | } 216 | } 217 | 218 | // ============================================================= 219 | // COLLECT VALIDATOR NAMES 220 | // ============================================================= 221 | 222 | // Get named validators 223 | if ($this.data("validation") !== undefined) { 224 | validatorNames = $this.data("validation").split(","); 225 | } 226 | 227 | // Get extra ones defined on the element's data attributes 228 | $.each($this.data(), function (i, el) { 229 | var parts = i.replace(/([A-Z])/g, ",$1").split(","); 230 | if (parts[0] === "validation" && parts[1]) { 231 | validatorNames.push(parts[1]); 232 | } 233 | }); 234 | 235 | // ============================================================= 236 | // NORMALISE VALIDATOR NAMES 237 | // ============================================================= 238 | 239 | var validatorNamesToInspect = validatorNames; 240 | var newValidatorNamesToInspect = []; 241 | 242 | do // repeatedly expand 'shortcut' validators into their real validators 243 | { 244 | // Uppercase only the first letter of each name 245 | $.each(validatorNames, function (i, el) { 246 | validatorNames[i] = formatValidatorName(el); 247 | }); 248 | 249 | // Remove duplicate validator names 250 | validatorNames = $.unique(validatorNames); 251 | 252 | // Pull out the new validator names from each shortcut 253 | newValidatorNamesToInspect = []; 254 | $.each(validatorNamesToInspect, function(i, el) { 255 | if ($this.data("validation" + el + "Shortcut") !== undefined) { 256 | // Are these custom validators? 257 | // Pull them out! 258 | $.each($this.data("validation" + el + "Shortcut").split(","), function(i2, el2) { 259 | newValidatorNamesToInspect.push(el2); 260 | }); 261 | } else if (settings.builtInValidators[el.toLowerCase()]) { 262 | // Is this a recognised built-in? 263 | // Pull it out! 264 | var validator = settings.builtInValidators[el.toLowerCase()]; 265 | if (validator.type.toLowerCase() === "shortcut") { 266 | $.each(validator.shortcut.split(","), function (i, el) { 267 | el = formatValidatorName(el); 268 | newValidatorNamesToInspect.push(el); 269 | validatorNames.push(el); 270 | }); 271 | } 272 | } 273 | }); 274 | 275 | validatorNamesToInspect = newValidatorNamesToInspect; 276 | 277 | } while (validatorNamesToInspect.length > 0) 278 | 279 | // ============================================================= 280 | // SET UP VALIDATOR ARRAYS 281 | // ============================================================= 282 | 283 | var validators = {}; 284 | 285 | $.each(validatorNames, function (i, el) { 286 | // Set up the 'override' message 287 | var message = $this.data("validation" + el + "Message"); 288 | var hasOverrideMessage = (message !== undefined); 289 | var foundValidator = false; 290 | message = 291 | ( 292 | message 293 | ? message 294 | : "'" + el + "' validation failed " 295 | ) 296 | ; 297 | 298 | $.each( 299 | settings.validatorTypes, 300 | function (validatorType, validatorTemplate) { 301 | if (validators[validatorType] === undefined) { 302 | validators[validatorType] = []; 303 | } 304 | if (!foundValidator && $this.data("validation" + el + formatValidatorName(validatorTemplate.name)) !== undefined) { 305 | validators[validatorType].push( 306 | $.extend( 307 | true, 308 | { 309 | name: formatValidatorName(validatorTemplate.name), 310 | message: message 311 | }, 312 | validatorTemplate.init($this, el) 313 | ) 314 | ); 315 | foundValidator = true; 316 | } 317 | } 318 | ); 319 | 320 | if (!foundValidator && settings.builtInValidators[el.toLowerCase()]) { 321 | 322 | var validator = $.extend(true, {}, settings.builtInValidators[el.toLowerCase()]); 323 | if (hasOverrideMessage) { 324 | validator.message = message; 325 | } 326 | var validatorType = validator.type.toLowerCase(); 327 | 328 | if (validatorType === "shortcut") { 329 | foundValidator = true; 330 | } else { 331 | $.each( 332 | settings.validatorTypes, 333 | function (validatorTemplateType, validatorTemplate) { 334 | if (validators[validatorTemplateType] === undefined) { 335 | validators[validatorTemplateType] = []; 336 | } 337 | if (!foundValidator && validatorType === validatorTemplateType.toLowerCase()) { 338 | $this.data("validation" + el + formatValidatorName(validatorTemplate.name), validator[validatorTemplate.name.toLowerCase()]); 339 | validators[validatorType].push( 340 | $.extend( 341 | validator, 342 | validatorTemplate.init($this, el) 343 | ) 344 | ); 345 | foundValidator = true; 346 | } 347 | } 348 | ); 349 | } 350 | } 351 | 352 | if (! foundValidator) { 353 | $.error("Cannot find validation info for '" + el + "'"); 354 | } 355 | }); 356 | 357 | // ============================================================= 358 | // STORE FALLBACK VALUES 359 | // ============================================================= 360 | 361 | $helpBlock.data( 362 | "original-contents", 363 | ( 364 | $helpBlock.data("original-contents") 365 | ? $helpBlock.data("original-contents") 366 | : $helpBlock.html() 367 | ) 368 | ); 369 | 370 | $helpBlock.data( 371 | "original-role", 372 | ( 373 | $helpBlock.data("original-role") 374 | ? $helpBlock.data("original-role") 375 | : $helpBlock.attr("role") 376 | ) 377 | ); 378 | 379 | $controlGroup.data( 380 | "original-classes", 381 | ( 382 | $controlGroup.data("original-clases") 383 | ? $controlGroup.data("original-classes") 384 | : $controlGroup.attr("class") 385 | ) 386 | ); 387 | 388 | $this.data( 389 | "original-aria-invalid", 390 | ( 391 | $this.data("original-aria-invalid") 392 | ? $this.data("original-aria-invalid") 393 | : $this.attr("aria-invalid") 394 | ) 395 | ); 396 | 397 | // ============================================================= 398 | // VALIDATION 399 | // ============================================================= 400 | 401 | $this.bind( 402 | "validation.validation", 403 | function (event, params) { 404 | 405 | var value = getValue($this); 406 | 407 | // Get a list of the errors to apply 408 | var errorsFound = []; 409 | 410 | $.each(validators, function (validatorType, validatorTypeArray) { 411 | if (value || value.length || (params && params.includeEmpty) || (!!settings.validatorTypes[validatorType].blockSubmit && params && !!params.submitting)) { 412 | $.each(validatorTypeArray, function (i, validator) { 413 | if (settings.validatorTypes[validatorType].validate($this, value, validator)) { 414 | errorsFound.push(validator.message); 415 | } 416 | }); 417 | } 418 | }); 419 | 420 | return errorsFound; 421 | } 422 | ); 423 | 424 | $this.bind( 425 | "getValidators.validation", 426 | function () { 427 | return validators; 428 | } 429 | ); 430 | 431 | // ============================================================= 432 | // WATCH FOR CHANGES 433 | // ============================================================= 434 | $this.bind( 435 | "submit.validation", 436 | function () { 437 | return $this.triggerHandler("change.validation", {submitting: true}); 438 | } 439 | ); 440 | $this.bind( 441 | [ 442 | "keyup", 443 | "focus", 444 | "blur", 445 | "click", 446 | "keydown", 447 | "keypress", 448 | "change" 449 | ].join(".validation ") + ".validation", 450 | function (e, params) { 451 | 452 | var value = getValue($this); 453 | 454 | var errorsFound = []; 455 | 456 | $controlGroup.find("input,textarea,select").each(function (i, el) { 457 | var oldCount = errorsFound.length; 458 | $.each($(el).triggerHandler("validation.validation", params), function (j, message) { 459 | errorsFound.push(message); 460 | }); 461 | if (errorsFound.length > oldCount) { 462 | $(el).attr("aria-invalid", "true"); 463 | } else { 464 | var original = $this.data("original-aria-invalid"); 465 | $(el).attr("aria-invalid", (original !== undefined ? original : false)); 466 | } 467 | }); 468 | 469 | $form.find("input,select,textarea").not($this).not("[name=\"" + $this.attr("name") + "\"]").trigger("validationLostFocus.validation"); 470 | 471 | errorsFound = $.unique(errorsFound.sort()); 472 | 473 | // Were there any errors? 474 | if (errorsFound.length) { 475 | // Better flag it up as a warning. 476 | $controlGroup.removeClass("success error").addClass("warning"); 477 | 478 | // How many errors did we find? 479 | if (settings.options.semanticallyStrict && errorsFound.length === 1) { 480 | // Only one? Being strict? Just output it. 481 | $helpBlock.html(errorsFound[0] + 482 | ( settings.options.prependExistingHelpBlock ? $helpBlock.data("original-contents") : "" )); 483 | } else { 484 | // Multiple? Being sloppy? Glue them together into an UL. 485 | $helpBlock.html("
  • " + errorsFound.join("
  • ") + "
" + 486 | ( settings.options.prependExistingHelpBlock ? $helpBlock.data("original-contents") : "" )); 487 | } 488 | } else { 489 | $controlGroup.removeClass("warning error success"); 490 | if (value.length > 0) { 491 | $controlGroup.addClass("success"); 492 | } 493 | $helpBlock.html($helpBlock.data("original-contents")); 494 | } 495 | 496 | if (e.type === "blur") { 497 | $controlGroup.removeClass("success"); 498 | } 499 | } 500 | ); 501 | $this.bind("validationLostFocus.validation", function () { 502 | $controlGroup.removeClass("success"); 503 | }); 504 | }); 505 | }, 506 | destroy : function( ) { 507 | 508 | return this.each( 509 | function() { 510 | 511 | var 512 | $this = $(this), 513 | $controlGroup = $this.parents(".control-group").first(), 514 | $helpBlock = $controlGroup.find(".help-block").first(); 515 | 516 | // remove our events 517 | $this.unbind('.validation'); // events are namespaced. 518 | // reset help text 519 | $helpBlock.html($helpBlock.data("original-contents")); 520 | // reset classes 521 | $controlGroup.attr("class", $controlGroup.data("original-classes")); 522 | // reset aria 523 | $this.attr("aria-invalid", $this.data("original-aria-invalid")); 524 | // reset role 525 | $helpBlock.attr("role", $this.data("original-role")); 526 | // remove all elements we created 527 | if (createdElements.indexOf($helpBlock[0]) > -1) { 528 | $helpBlock.remove(); 529 | } 530 | 531 | } 532 | ); 533 | 534 | }, 535 | collectErrors : function(includeEmpty) { 536 | 537 | var errorMessages = {}; 538 | this.each(function (i, el) { 539 | var $el = $(el); 540 | var name = $el.attr("name"); 541 | var errors = $el.triggerHandler("validation.validation", {includeEmpty: true}); 542 | errorMessages[name] = $.extend(true, errors, errorMessages[name]); 543 | }); 544 | 545 | $.each(errorMessages, function (i, el) { 546 | if (el.length === 0) { 547 | delete errorMessages[i]; 548 | } 549 | }); 550 | 551 | return errorMessages; 552 | 553 | }, 554 | hasErrors: function() { 555 | 556 | var errorMessages = []; 557 | 558 | this.each(function (i, el) { 559 | errorMessages = errorMessages.concat( 560 | $(el).triggerHandler("getValidators.validation") ? $(el).triggerHandler("validation.validation", {submitting: true}) : [] 561 | ); 562 | }); 563 | 564 | return (errorMessages.length > 0); 565 | }, 566 | override : function (newDefaults) { 567 | defaults = $.extend(true, defaults, newDefaults); 568 | } 569 | }, 570 | validatorTypes: { 571 | callback: { 572 | name: "callback", 573 | init: function ($this, name) { 574 | return { 575 | validatorName: name, 576 | callback: $this.data("validation" + name + "Callback"), 577 | lastValue: $this.val(), 578 | lastValid: true, 579 | lastFinished: true 580 | }; 581 | }, 582 | validate: function ($this, value, validator) { 583 | if (validator.lastValue === value && validator.lastFinished) { 584 | return !validator.lastValid; 585 | } 586 | 587 | if (validator.lastFinished === true) 588 | { 589 | validator.lastValue = value; 590 | validator.lastValid = true; 591 | validator.lastFinished = false; 592 | 593 | var rrjqbvValidator = validator; 594 | var rrjqbvThis = $this; 595 | executeFunctionByName( 596 | validator.callback, 597 | window, 598 | $this, 599 | value, 600 | function (data) { 601 | if (rrjqbvValidator.lastValue === data.value) { 602 | rrjqbvValidator.lastValid = data.valid; 603 | if (data.message) { 604 | rrjqbvValidator.message = data.message; 605 | } 606 | rrjqbvValidator.lastFinished = true; 607 | rrjqbvThis.data("validation" + rrjqbvValidator.validatorName + "Message", rrjqbvValidator.message); 608 | // Timeout is set to avoid problems with the events being considered 'already fired' 609 | setTimeout(function () { 610 | rrjqbvThis.trigger("change.validation"); 611 | }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst 612 | } 613 | } 614 | ); 615 | } 616 | 617 | return false; 618 | 619 | } 620 | }, 621 | ajax: { 622 | name: "ajax", 623 | init: function ($this, name) { 624 | return { 625 | validatorName: name, 626 | url: $this.data("validation" + name + "Ajax"), 627 | lastValue: $this.val(), 628 | lastValid: true, 629 | lastFinished: true 630 | }; 631 | }, 632 | validate: function ($this, value, validator) { 633 | if (""+validator.lastValue === ""+value && validator.lastFinished === true) { 634 | return validator.lastValid === false; 635 | } 636 | 637 | if (validator.lastFinished === true) 638 | { 639 | validator.lastValue = value; 640 | validator.lastValid = true; 641 | validator.lastFinished = false; 642 | $.ajax({ 643 | url: validator.url, 644 | data: "value=" + value + "&field=" + $this.attr("name"), 645 | dataType: "json", 646 | success: function (data) { 647 | if (""+validator.lastValue === ""+data.value) { 648 | validator.lastValid = !!(data.valid); 649 | if (data.message) { 650 | validator.message = data.message; 651 | } 652 | validator.lastFinished = true; 653 | $this.data("validation" + validator.validatorName + "Message", validator.message); 654 | // Timeout is set to avoid problems with the events being considered 'already fired' 655 | setTimeout(function () { 656 | $this.trigger("change.validation"); 657 | }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst 658 | } 659 | }, 660 | failure: function () { 661 | validator.lastValid = true; 662 | validator.message = "ajax call failed"; 663 | validator.lastFinished = true; 664 | $this.data("validation" + validator.validatorName + "Message", validator.message); 665 | // Timeout is set to avoid problems with the events being considered 'already fired' 666 | setTimeout(function () { 667 | $this.trigger("change.validation"); 668 | }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst 669 | } 670 | }); 671 | } 672 | 673 | return false; 674 | 675 | } 676 | }, 677 | regex: { 678 | name: "regex", 679 | init: function ($this, name) { 680 | return {regex: regexFromString($this.data("validation" + name + "Regex"))}; 681 | }, 682 | validate: function ($this, value, validator) { 683 | return (!validator.regex.test(value) && ! validator.negative) 684 | || (validator.regex.test(value) && validator.negative); 685 | } 686 | }, 687 | required: { 688 | name: "required", 689 | init: function ($this, name) { 690 | return {}; 691 | }, 692 | validate: function ($this, value, validator) { 693 | return !!(value.length === 0 && ! validator.negative) 694 | || !!(value.length > 0 && validator.negative); 695 | }, 696 | blockSubmit: true 697 | }, 698 | match: { 699 | name: "match", 700 | init: function ($this, name) { 701 | var element = $this.parents("form").first().find("[name=\"" + $this.data("validation" + name + "Match") + "\"]").first(); 702 | element.bind("validation.validation", function () { 703 | $this.trigger("change.validation", {submitting: true}); 704 | }); 705 | return {"element": element}; 706 | }, 707 | validate: function ($this, value, validator) { 708 | return (value !== validator.element.val() && ! validator.negative) 709 | || (value === validator.element.val() && validator.negative); 710 | }, 711 | blockSubmit: true 712 | }, 713 | max: { 714 | name: "max", 715 | init: function ($this, name) { 716 | return {max: $this.data("validation" + name + "Max")}; 717 | }, 718 | validate: function ($this, value, validator) { 719 | return (parseFloat(value, 10) > parseFloat(validator.max, 10) && ! validator.negative) 720 | || (parseFloat(value, 10) <= parseFloat(validator.max, 10) && validator.negative); 721 | } 722 | }, 723 | min: { 724 | name: "min", 725 | init: function ($this, name) { 726 | return {min: $this.data("validation" + name + "Min")}; 727 | }, 728 | validate: function ($this, value, validator) { 729 | return (parseFloat(value) < parseFloat(validator.min) && ! validator.negative) 730 | || (parseFloat(value) >= parseFloat(validator.min) && validator.negative); 731 | } 732 | }, 733 | maxlength: { 734 | name: "maxlength", 735 | init: function ($this, name) { 736 | return {maxlength: $this.data("validation" + name + "Maxlength")}; 737 | }, 738 | validate: function ($this, value, validator) { 739 | return ((value.length > validator.maxlength) && ! validator.negative) 740 | || ((value.length <= validator.maxlength) && validator.negative); 741 | } 742 | }, 743 | minlength: { 744 | name: "minlength", 745 | init: function ($this, name) { 746 | return {minlength: $this.data("validation" + name + "Minlength")}; 747 | }, 748 | validate: function ($this, value, validator) { 749 | return ((value.length < validator.minlength) && ! validator.negative) 750 | || ((value.length >= validator.minlength) && validator.negative); 751 | } 752 | }, 753 | maxchecked: { 754 | name: "maxchecked", 755 | init: function ($this, name) { 756 | var elements = $this.parents("form").first().find("[name=\"" + $this.attr("name") + "\"]"); 757 | elements.bind("click.validation", function () { 758 | $this.trigger("change.validation", {includeEmpty: true}); 759 | }); 760 | return {maxchecked: $this.data("validation" + name + "Maxchecked"), elements: elements}; 761 | }, 762 | validate: function ($this, value, validator) { 763 | return (validator.elements.filter(":checked").length > validator.maxchecked && ! validator.negative) 764 | || (validator.elements.filter(":checked").length <= validator.maxchecked && validator.negative); 765 | }, 766 | blockSubmit: true 767 | }, 768 | minchecked: { 769 | name: "minchecked", 770 | init: function ($this, name) { 771 | var elements = $this.parents("form").first().find("[name=\"" + $this.attr("name") + "\"]"); 772 | elements.bind("click.validation", function () { 773 | $this.trigger("change.validation", {includeEmpty: true}); 774 | }); 775 | return {minchecked: $this.data("validation" + name + "Minchecked"), elements: elements}; 776 | }, 777 | validate: function ($this, value, validator) { 778 | return (validator.elements.filter(":checked").length < validator.minchecked && ! validator.negative) 779 | || (validator.elements.filter(":checked").length >= validator.minchecked && validator.negative); 780 | }, 781 | blockSubmit: true 782 | } 783 | }, 784 | builtInValidators: { 785 | email: { 786 | name: "Email", 787 | type: "shortcut", 788 | shortcut: "validemail" 789 | }, 790 | validemail: { 791 | name: "Validemail", 792 | type: "regex", 793 | regex: "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\.[A-Za-z]{2,4}", 794 | message: "Not a valid email address" 795 | }, 796 | passwordagain: { 797 | name: "Passwordagain", 798 | type: "match", 799 | match: "password", 800 | message: "Does not match the given password" 801 | }, 802 | positive: { 803 | name: "Positive", 804 | type: "shortcut", 805 | shortcut: "number,positivenumber" 806 | }, 807 | negative: { 808 | name: "Negative", 809 | type: "shortcut", 810 | shortcut: "number,negativenumber" 811 | }, 812 | number: { 813 | name: "Number", 814 | type: "regex", 815 | regex: "([+-]?\\\d+(\\\.\\\d*)?([eE][+-]?[0-9]+)?)?", 816 | message: "Must be a number" 817 | }, 818 | integer: { 819 | name: "Integer", 820 | type: "regex", 821 | regex: "[+-]?\\\d+", 822 | message: "No decimal places allowed" 823 | }, 824 | positivenumber: { 825 | name: "Positivenumber", 826 | type: "min", 827 | min: 0, 828 | message: "Must be a positive number" 829 | }, 830 | negativenumber: { 831 | name: "Negativenumber", 832 | type: "max", 833 | max: 0, 834 | message: "Must be a negative number" 835 | }, 836 | required: { 837 | name: "Required", 838 | type: "required", 839 | message: "This is required" 840 | }, 841 | checkone: { 842 | name: "Checkone", 843 | type: "minchecked", 844 | minchecked: 1, 845 | message: "Check at least one option" 846 | } 847 | } 848 | }; 849 | 850 | var formatValidatorName = function (name) { 851 | return name 852 | .toLowerCase() 853 | .replace( 854 | /(^|\s)([a-z])/g , 855 | function(m,p1,p2) { 856 | return p1+p2.toUpperCase(); 857 | } 858 | ) 859 | ; 860 | }; 861 | 862 | var getValue = function ($this) { 863 | // Extract the value we're talking about 864 | var value = $this.val(); 865 | var type = $this.attr("type"); 866 | if (type === "checkbox") { 867 | value = ($this.is(":checked") ? value : ""); 868 | } 869 | if (type === "radio") { 870 | value = ($('input[name="' + $this.attr("name") + '"]:checked').length > 0 ? value : ""); 871 | } 872 | return value; 873 | }; 874 | 875 | function regexFromString(inputstring) { 876 | return new RegExp("^" + inputstring + "$"); 877 | } 878 | 879 | /** 880 | * Thanks to Jason Bunting via StackOverflow.com 881 | * 882 | * http://stackoverflow.com/questions/359788/how-to-execute-a-javascript-function-when-i-have-its-name-as-a-string#answer-359910 883 | * Short link: http://tinyurl.com/executeFunctionByName 884 | **/ 885 | function executeFunctionByName(functionName, context /*, args*/) { 886 | var args = Array.prototype.slice.call(arguments).splice(2); 887 | var namespaces = functionName.split("."); 888 | var func = namespaces.pop(); 889 | for(var i = 0; i < namespaces.length; i++) { 890 | context = context[namespaces[i]]; 891 | } 892 | return context[func].apply(this, args); 893 | } 894 | 895 | $.fn.jqBootstrapValidation = function( method ) { 896 | 897 | if ( defaults.methods[method] ) { 898 | return defaults.methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); 899 | } else if ( typeof method === 'object' || ! method ) { 900 | return defaults.methods.init.apply( this, arguments ); 901 | } else { 902 | $.error( 'Method ' + method + ' does not exist on jQuery.jqBootstrapValidation' ); 903 | return null; 904 | } 905 | 906 | }; 907 | 908 | $.jqBootstrapValidation = function (options) { 909 | $(":input").not("[type=image],[type=submit]").jqBootstrapValidation.apply(this,arguments); 910 | }; 911 | 912 | })( jQuery ); 913 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Explore Data Science Academy - Serverless Student Portfolio Web page 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 40 | 41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 |

Cloud Computing Predict - Project Portfolio

50 | 51 | 52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 |

Machine Learning - Time Series - Dashboarding

61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 |

Portfolio

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 |
117 |
118 |
119 | 120 | 121 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 |
130 |
131 | 132 | 133 | 134 | 135 |
136 |
137 | 138 |
139 |
140 |
141 |
142 |
143 | 144 | 145 | 146 | 147 |
148 |
149 |
150 |
151 |
152 | 153 |
154 |
155 | 156 |

About

157 | 158 |
159 |
160 |
161 |
162 |
163 | 164 |
165 | 166 | 167 |

EXPLORE is a next generation learning institution that teaches students the skills of the future. From Data Science to Data Engineering, from Machine Learning to Deep Learning we deliver cutting edge courses to satisfy your hunger to learn.

168 |

Our philosophy is to teach our students how to solve problems in the real world. We emphasise teamwork, collaboration and working within constraints, under deadlines … while understanding context, audience and implementation challenges.

169 | 170 |
171 | 172 | 178 |
179 |
180 | 181 | 182 |
183 |
184 | 185 |

Contact Me

186 | 187 |
188 |
189 |
190 |
191 |
192 | 193 |
194 |
195 |
196 |
197 |
198 | 199 | 200 |

201 |
202 |
203 |
204 |
205 | 206 | 207 |

208 |
209 |
210 |
211 |
212 | 213 | 214 |

215 |
216 |
217 |
218 |
219 | 220 | 221 |

222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 | 233 | 234 |
235 |
236 |
237 | 238 | 239 | 240 |
241 |

Location

242 |

243 | 12th Floor, Rennie House, 19 Ameshoff St 244 |
245 | Johannesburg, 2001 246 |

247 |
248 | 249 | 250 |
251 |

Around the Web

252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 |
260 | 261 | 262 | 263 |
264 |

Explore AI Cloud Computing Predict

265 |

266 | Create your own data science portfolio with an intelligent NPL bot handling enquiries for you. 267 |

268 |
269 | 270 |
271 |
272 |
273 | 274 | 277 | 278 |
279 | 280 |
281 | 282 | 283 | 284 | 285 | 321 | 322 | 323 | 324 | 325 | 326 | 362 | 363 | 364 | 365 | 366 | 367 | 403 | 404 | 405 | 406 | 407 | 408 | 444 | 445 | 446 | 447 | 448 | 449 | 485 | 486 | 487 | 488 | 489 | 490 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | -------------------------------------------------------------------------------- /js/scripts.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Freelancer v6.0.5 (https://startbootstrap.com/theme/freelancer) 3 | * Copyright 2013-2020 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-freelancer/blob/master/LICENSE) 5 | */ 6 | (function($) { 7 | "use strict"; // Start of use strict 8 | 9 | // Smooth scrolling using jQuery easing 10 | $('a.js-scroll-trigger[href*="#"]:not([href="#"])').click(function() { 11 | if (location.pathname.replace(/^\//, '') == this.pathname.replace(/^\//, '') && location.hostname == this.hostname) { 12 | var target = $(this.hash); 13 | target = target.length ? target : $('[name=' + this.hash.slice(1) + ']'); 14 | if (target.length) { 15 | $('html, body').animate({ 16 | scrollTop: (target.offset().top - 71) 17 | }, 1000, "easeInOutExpo"); 18 | return false; 19 | } 20 | } 21 | }); 22 | 23 | // Scroll to top button appear 24 | $(document).scroll(function() { 25 | var scrollDistance = $(this).scrollTop(); 26 | if (scrollDistance > 100) { 27 | $('.scroll-to-top').fadeIn(); 28 | } else { 29 | $('.scroll-to-top').fadeOut(); 30 | } 31 | }); 32 | 33 | // Closes responsive menu when a scroll trigger link is clicked 34 | $('.js-scroll-trigger').click(function() { 35 | $('.navbar-collapse').collapse('hide'); 36 | }); 37 | 38 | // Activate scrollspy to add active class to navbar items on scroll 39 | $('body').scrollspy({ 40 | target: '#mainNav', 41 | offset: 80 42 | }); 43 | 44 | // Collapse Navbar 45 | var navbarCollapse = function() { 46 | if ($("#mainNav").offset().top > 100) { 47 | $("#mainNav").addClass("navbar-shrink"); 48 | } else { 49 | $("#mainNav").removeClass("navbar-shrink"); 50 | } 51 | }; 52 | // Collapse now if page is not at top 53 | navbarCollapse(); 54 | // Collapse the navbar when page is scrolled 55 | $(window).scroll(navbarCollapse); 56 | 57 | // Floating label headings for the contact form 58 | $(function() { 59 | $("body").on("input propertychange", ".floating-label-form-group", function(e) { 60 | $(this).toggleClass("floating-label-form-group-with-value", !!$(e.target).val()); 61 | }).on("focus", ".floating-label-form-group", function() { 62 | $(this).addClass("floating-label-form-group-with-focus"); 63 | }).on("blur", ".floating-label-form-group", function() { 64 | $(this).removeClass("floating-label-form-group-with-focus"); 65 | }); 66 | }); 67 | 68 | })(jQuery); // End of use strict 69 | -------------------------------------------------------------------------------- /student_solution_files/Example_AWS_Comprehend_Key_Phrases_Output.txt: -------------------------------------------------------------------------------- 1 | {'KeyPhrases': [{'Score': 0.9999733567237854, 2 | 'Text': 'Hi Explore Data Science Student', 3 | 'BeginOffset': 2, 4 | 'EndOffset': 33}, 5 | {'Score': 0.9999999403953552, 6 | 'Text': 'your website', 7 | 'BeginOffset': 43, 8 | 'EndOffset': 55}, 9 | {'Score': 0.9958839416503906, 10 | 'Text': 'your portfolio projects', 11 | 'BeginOffset': 60, 12 | 'EndOffset': 83}, 13 | {'Score': 0.9999998807907104, 14 | 'Text': 'a look', 15 | 'BeginOffset': 96, 16 | 'EndOffset': 102}, 17 | {'Score': 1.0, 18 | 'Text': 'your GitHub page', 19 | 'BeginOffset': 106, 20 | 'EndOffset': 122}, 21 | {'Score': 0.9999998807907104, 22 | 'Text': 'the quality', 23 | 'BeginOffset': 153, 24 | 'EndOffset': 164}, 25 | {'Score': 0.9999991059303284, 26 | 'Text': 'your work', 27 | 'BeginOffset': 168, 28 | 'EndOffset': 177}, 29 | {'Score': 0.9999063014984131, 30 | 'Text': 'Your medium articles', 31 | 'BeginOffset': 180, 32 | 'EndOffset': 200}, 33 | {'Score': 0.9999991059303284, 34 | 'Text': 'my attention', 35 | 'BeginOffset': 215, 36 | 'EndOffset': 227}, 37 | {'Score': 0.9999028444290161, 38 | 'Text': 'your CV', 39 | 'BeginOffset': 258, 40 | 'EndOffset': 265}, 41 | {'Score': 0.9999997019767761, 42 | 'Text': 'a potential job', 43 | 'BeginOffset': 296, 44 | 'EndOffset': 311}, 45 | {'Score': 0.9999991059303284, 46 | 'Text': 'my clients', 47 | 'BeginOffset': 322, 48 | 'EndOffset': 332}, 49 | {'Score': 0.9999960064888, 50 | 'Text': 'a six week contract', 51 | 'BeginOffset': 344, 52 | 'EndOffset': 363}, 53 | {'Score': 0.9999924302101135, 54 | 'Text': 'your experience and skills', 55 | 'BeginOffset': 380, 56 | 'EndOffset': 406}, 57 | {'Score': 0.9999914765357971, 58 | 'Text': 'the job specifications', 59 | 'BeginOffset': 413, 60 | 'EndOffset': 435}, 61 | {'Score': 0.9999973177909851, 62 | 'Text': 'your time', 63 | 'BeginOffset': 470, 64 | 'EndOffset': 479}, 65 | {'Score': 0.9784839749336243, 66 | 'Text': 'Kind regards', 67 | 'BeginOffset': 483, 68 | 'EndOffset': 495}, 69 | {'Score': 0.9996178150177002, 70 | 'Text': 'Charlotte Regression\n \nSenior HR Manager', 71 | 'BeginOffset': 499, 72 | 'EndOffset': 536}], 73 | 'ResponseMetadata': {'RequestId': '7e96373a-7381-45c9-beaf-beb47976002d', 74 | 'HTTPStatusCode': 200, 75 | 'HTTPHeaders': {'x-amzn-requestid': '7e96373a-7381-45c9-beaf-beb47976002d', 76 | 'content-type': 'application/x-amz-json-1.1', 77 | 'content-length': '1603', 78 | 'date': 'Wed, 24 Feb 2021 11:43:45 GMT'}, 79 | 'RetryAttempts': 0}} -------------------------------------------------------------------------------- /student_solution_files/Example_AWS_Comprehend_Sentiment_Output.txt: -------------------------------------------------------------------------------- 1 | {'Sentiment': 'POSITIVE', 2 | 'SentimentScore': {'Positive': 0.9608212113380432, 3 | 'Negative': 5.171292650629766e-05, 4 | 'Neutral': 0.039048053324222565, 5 | 'Mixed': 7.912805449450389e-05}, 6 | 'ResponseMetadata': {'RequestId': 'a34c897f-d364-4b51-8c04-89f409cfd1a0', 7 | 'HTTPStatusCode': 200, 8 | 'HTTPHeaders': {'x-amzn-requestid': 'a34c897f-d364-4b51-8c04-89f409cfd1a0', 9 | 'content-type': 'application/x-amz-json-1.1', 10 | 'content-length': '165', 11 | 'date': 'Wed, 24 Feb 2021 11:43:45 GMT'}, 12 | 'RetryAttempts': 0}} -------------------------------------------------------------------------------- /student_solution_files/aggregated_lambda_function.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Final AWS Lambda function skeleton. 4 | 5 | Author: Explore Data Science Academy. 6 | 7 | Note: 8 | --------------------------------------------------------------------- 9 | The contents of this file should be added to a AWS Lambda function 10 | created as part of the EDSA Cloud-Computing Predict. 11 | For further guidance around this process, see the README instruction 12 | file which sits at the root of this repo. 13 | --------------------------------------------------------------------- 14 | 15 | """ 16 | 17 | # Lambda dependencies 18 | import boto3 # Python AWS SDK 19 | import json # Used for handling API-based data. 20 | import base64 # Needed to decode the incoming POST data 21 | import numpy as np # Array manipulation 22 | # <<< You will need to add additional libraries to complete this script >>> 23 | 24 | # ** Insert key phrases function ** 25 | # --- Insert your code here --- 26 | 27 | # ----------------------------- 28 | 29 | # ** Insert sentiment extraction function ** 30 | # --- Insert your code here --- 31 | 32 | # ----------------------------- 33 | 34 | # ** Insert email responses function ** 35 | # --- Insert your code here --- 36 | 37 | # ----------------------------- 38 | 39 | # Lambda function orchestrating the entire predict logic 40 | def lambda_handler(event, context): 41 | 42 | # Perform JSON data decoding 43 | body_enc = event['body'] 44 | dec_dict = json.loads(base64.b64decode(body_enc)) 45 | 46 | 47 | # ** Insert code to write to dynamodb ** 48 | # <<< Ensure that the DynamoDB write response object is saved 49 | # as the variable `db_response` >>> 50 | # --- Insert your code here --- 51 | 52 | 53 | # Do not change the name of this variable 54 | db_response = None 55 | # ----------------------------- 56 | 57 | 58 | # --- Amazon Comprehend --- 59 | comprehend = boto3.client(service_name='comprehend') 60 | 61 | # --- Insert your code here --- 62 | enquiry_text = None # <--- Insert code to place the website message into this variable 63 | # ----------------------------- 64 | 65 | # --- Insert your code here --- 66 | sentiment = None # <---Insert code to get the sentiment with AWS comprehend 67 | # ----------------------------- 68 | 69 | # --- Insert your code here --- 70 | key_phrases = None # <--- Insert code to get the key phrases with AWS comprehend 71 | # ----------------------------- 72 | 73 | # Get list of phrases in numpy array 74 | phrase = [] 75 | for i in range(0, len(key_phrases['KeyPhrases'])-1): 76 | phrase = np.append(phrase, key_phrases['KeyPhrases'][i]['Text']) 77 | 78 | 79 | # ** Use the `email_response` function to generate the text for your email response ** 80 | # <<< Ensure that the response text is stored in the variable `email_text` >>> 81 | # --- Insert your code here --- 82 | # Do not change the name of this variable 83 | email_text = None 84 | 85 | 86 | # ----------------------------- 87 | 88 | 89 | # ** SES Functionality ** 90 | 91 | # Insert code to send an email, using AWS SES, with the above defined 92 | # `email_text` variable as it's body. 93 | # <<< Ensure that the SES service response is stored in the variable `ses_response` >>> 94 | # --- Insert your code here --- 95 | 96 | # Do not change the name of this variable 97 | ses_response = None 98 | 99 | # ... 100 | 101 | # Do not modify the email subject line 102 | SUBJECT = f"Data Science Portfolio Project Website - Hello {dec_dict['name']}" 103 | 104 | # ----------------------------- 105 | 106 | 107 | # ** Create a response object to inform the website that the 108 | # workflow executed successfully. Note that this object is 109 | # used during predict marking and should not be modified.** 110 | # --- DO NOT MODIFY THIS CODE --- 111 | lambda_response = { 112 | 'statusCode': 200, 113 | 'body': json.dumps({ 114 | 'Name': dec_dict['name'], 115 | 'Email': dec_dict['email'], 116 | 'Cell': dec_dict['phone'], 117 | 'Message': dec_dict['message'], 118 | 'DB_response': db_response, 119 | 'SES_response': ses_response, 120 | 'Email_message': email_text 121 | }) 122 | } 123 | # ----------------------------- 124 | 125 | return lambda_response 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /student_solution_files/basic_lambda_data_decoding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Initial AWS Lambda function is used to decode POST-request data received from the 3 | student portfolio website. 4 | 5 | Author: Explore Data Science Academy. 6 | 7 | Note: 8 | --------------------------------------------------------------------- 9 | The contents of this file should be added to a AWS Lambda function 10 | created as part of the EDSA Cloud-Computing Predict. 11 | For further guidance around this process, see the README instruction 12 | file which sits at the root of this repo. 13 | --------------------------------------------------------------------- 14 | 15 | """ 16 | 17 | # Lambda dependencies 18 | import boto3 # Python AWS SDK 19 | import json # Used for handling API-based data. 20 | import base64 # Needed to decode the incoming POST data 21 | 22 | def lambda_handler(event, context): 23 | 24 | # Perform JSON data decoding 25 | body_enc = event['body'] 26 | dec_dict = json.loads(base64.b64decode(body_enc)) 27 | 28 | # Note that all of the POST data from our website form can now 29 | # be accessed via the `dec_dict` dictionary object. 30 | # For example, if we entered the name field as 'Student_name on the website' : 31 | # >>> dec_dict['name'] 32 | # 'Student_name' 33 | 34 | # Create a response object to tell the website that the form data 35 | # was successfully received. We use the contents of the decoded JSON dictionary 36 | # to create this response. As the predict progresses, we'll include 37 | # more information about the AWS services we will invoke. 38 | lambda_response = { 39 | 'statusCode': 200, # <-- Tells the website that everything executed successfully. 40 | 'body': json.dumps({ 41 | 'Name': dec_dict['name'], 42 | 'Email': dec_dict['email'], 43 | 'Cell': dec_dict['phone'], 44 | 'Message': dec_dict['message'] 45 | }) 46 | } 47 | 48 | return lambda_response 49 | -------------------------------------------------------------------------------- /student_solution_files/email_responses.py: -------------------------------------------------------------------------------- 1 | """ 2 | Function used to construct an intelligent response to a given input message. 3 | 4 | 5 | Author: Explore Data Science Academy. 6 | 7 | Note: 8 | --------------------------------------------------------------------- 9 | The contents of this file should be added to a AWS Lambda function 10 | created as part of the EDSA Cloud-Computing Predict. 11 | For further guidance around this process, see the README instruction 12 | file which sits at the root of this repo. 13 | --------------------------------------------------------------------- 14 | 15 | Description: This function uses the `find_max_sentiment` and `key_phrase_finder` 16 | functions to firstly extract the overwhelming sentiment of a message reported 17 | by AWS Comprehend, and secondly see if the key phrases determined by the service 18 | match with a supplied list of user phrases. Given the sentiment and the boolean 19 | output of the `key_phrase_finder` function, we can then use conditional logic 20 | to craft an intelligent response. 21 | 22 | """ 23 | 24 | def email_response(name, critical_phrase_list, list_of_extracted_phrases, AWS_Comprehend_Sentiment_Dump): 25 | 26 | # Function Constants 27 | SENDER_NAME = 'Place your name here' 28 | 29 | # --- Check for the sentiment of the message and find dominant sentiment score --- 30 | Sentiment_finder = find_max_sentiment(AWS_Comprehend_Sentiment_Dump) 31 | overwhelming_sentiment = Sentiment_finder[0] 32 | overwhelming_sentiment_score = Sentiment_finder[1] 33 | 34 | # --- Check for article critical phrases --- 35 | Phrase_Matcher_Article = key_phrase_finder(critical_phrase_list, list_of_extracted_phrases) 36 | Matched_Phrases_Article = Phrase_Matcher_Article[0] 37 | Matched_Phrases_Checker_Article = Phrase_Matcher_Article[1] 38 | 39 | # --- Check for project phrases --- 40 | Phrase_Matcher_Project = key_phrase_finder(['github', 'git', 'Git', 41 | 'GitHub', 'projects', 42 | 'portfolio', 'Portfolio'], 43 | list_of_extracted_phrases) 44 | Matched_Phrases_Project = Phrase_Matcher_Project[0] 45 | Matched_Phrases_Checker_Project = Phrase_Matcher_Project[1] 46 | 47 | # --- Check for C.V phrases --- 48 | Phrase_Matcher_CV = key_phrase_finder(['C.V', 'resume', 'Curriculum Vitae', 49 | 'Resume', 'CV'], 50 | list_of_extracted_phrases) 51 | Matched_Phrases_CV = Phrase_Matcher_CV[0] 52 | Matched_Phrases_Checker_CV = Phrase_Matcher_CV[1] 53 | 54 | # --- Generate standard responses --- 55 | # === DO NOT MODIFY THIS TEXT FOR THE PURPOSE OF PREDICT ASSESSMENT === 56 | Greetings_text = f'Good day {name},' 57 | 58 | CV_text = 'I see that you mentioned my C.V in your message. \ 59 | I am happy to forward you my C.V in response. \ 60 | If you have any other questions or C.V related queries please do get in touch. ' 61 | 62 | Project_Text = 'The projects I listed on my site only include \ 63 | the ones not running in production. I have \ 64 | several other projects that might interest you.' 65 | 66 | Article_Text = 'In your message you mentioned my blog posts and data science articles. \ 67 | I have several other articles published in academic journals. \ 68 | Please do let me know if you are interested - I am happy to forward them to you' 69 | 70 | Negative_Text = f'I see that you are unhappy in your response. \ 71 | Can we please set up a session to discuss why you are not happy, \ 72 | be it with the website, my personal projects or anything else. \ 73 | \n\nLooking forward to our discussion. \n\nKind Regards, \n\nMy Name' 74 | 75 | Neutral_Text = f'Thank you for your email. Let me know if you need any additional information.\ 76 | \n\nKind Regards, \n\n{SENDER_NAME}' 77 | 78 | Farewell_Text = f'Thank you for your email.\n\nIf there is anything else I can assist \ 79 | you with please let me know and I will set up a meeting for us to meet\ 80 | in person.\n\nKind Regards, \n\n{SENDER_NAME}' 81 | # ===================================================================== 82 | 83 | # --- Email Logic --- 84 | if overwhelming_sentiment == 'POSITIVE': 85 | if ((Matched_Phrases_Checker_CV == True) & \ 86 | (Matched_Phrases_Checker_Article == True) & \ 87 | (Matched_Phrases_Checker_Project == True)): 88 | 89 | mytuple = (Greetings_text, CV_text, Article_Text, Project_Text, Farewell_Text) 90 | Text = "\n \n".join(mytuple) 91 | 92 | elif ((Matched_Phrases_Checker_CV == True) & \ 93 | (Matched_Phrases_Checker_Article == False) & \ 94 | (Matched_Phrases_Checker_Project == True)): 95 | 96 | mytuple = (Greetings_text, CV_text, Project_Text, Farewell_Text) 97 | Text = "\n \n".join(mytuple) 98 | 99 | elif ((Matched_Phrases_Checker_CV == True) & \ 100 | (Matched_Phrases_Checker_Article == False) & \ 101 | (Matched_Phrases_Checker_Project == False)): 102 | 103 | mytuple = (Greetings_text, CV_text, Farewell_Text) 104 | Text = "\n \n".join(mytuple) 105 | 106 | elif ((Matched_Phrases_Checker_CV == False) & \ 107 | (Matched_Phrases_Checker_Article == True) & \ 108 | (Matched_Phrases_Checker_Project == False)): 109 | 110 | mytuple = (Greetings_text, Article_Text, Farewell_Text) 111 | Text = "\n \n".join(mytuple) 112 | 113 | elif ((Matched_Phrases_Checker_CV == False) & \ 114 | (Matched_Phrases_Checker_Article == False) & \ 115 | (Matched_Phrases_Checker_Project == False)): 116 | 117 | mytuple = (Greetings_text, Farewell_Text) 118 | Text = "\n \n".join(mytuple) 119 | 120 | elif ((Matched_Phrases_Checker_CV == False) & \ 121 | (Matched_Phrases_Checker_Article == False) & \ 122 | (Matched_Phrases_Checker_Project == True)): 123 | 124 | mytuple = (Greetings_text, Project_Text ,Farewell_Text) 125 | Text = "\n \n".join(mytuple) 126 | 127 | elif ((Matched_Phrases_Checker_CV == True) & \ 128 | (Matched_Phrases_Checker_Article == True) & \ 129 | (Matched_Phrases_Checker_Project == False)): 130 | 131 | mytuple = (Greetings_text, CV_text, Article_Text, Farewell_Text) 132 | Text = "\n \n".join(mytuple) 133 | 134 | else: 135 | mytuple = (Greetings_text, Project_Text, Article_Text, Farewell_Text) 136 | Text = "\n \n".join(mytuple) 137 | 138 | elif overwhelming_sentiment == 'NEGATIVE': 139 | mytuple = (Greetings_text, Negative_Text) 140 | Text = "\n \n".join(mytuple) 141 | 142 | else: 143 | mytuple = (Greetings_text, Neutral_Text) 144 | Text = "\n \n".join(mytuple) 145 | 146 | return Text 147 | -------------------------------------------------------------------------------- /student_solution_files/find_key_phrases.py: -------------------------------------------------------------------------------- 1 | """ 2 | Function used to match words in a supplied list with those in a AWS Comprehend 3 | generated dictionary containing the key phrases. 4 | 5 | Author: Explore Data Science Academy. 6 | 7 | Note: 8 | --------------------------------------------------------------------- 9 | The contents of this file should be added to a AWS Lambda function 10 | created as part of the EDSA Cloud-Computing Predict. 11 | For further guidance around this process, see the README instruction 12 | file which sits at the root of this repo. 13 | --------------------------------------------------------------------- 14 | 15 | """ 16 | 17 | # Function dependencies 18 | import numpy as np 19 | 20 | def key_phrase_finder(list_of_important_phrases, list_of_extracted_phrases): 21 | 22 | listing = [] 23 | PhraseChecker = None 24 | 25 | res = str(list_of_extracted_phrases).split() 26 | 27 | for important_word in list_of_important_phrases: 28 | names = res 29 | names2 = [word for word in names if important_word in word] 30 | isnot_empty = np.array(names2).size > 0 31 | 32 | if isnot_empty == True: 33 | listing = np.append(listing, names2) 34 | 35 | else: 36 | listing = listing 37 | 38 | if np.array(listing).size > 0: 39 | PhraseChecker = True 40 | 41 | else: 42 | PhraseChecker = False 43 | 44 | return listing, PhraseChecker 45 | -------------------------------------------------------------------------------- /student_solution_files/find_maximum_sentiment.py: -------------------------------------------------------------------------------- 1 | """ 2 | Function used to extract the overwhelming sentiment of a message. 3 | 4 | Author: Explore Data Science Academy. 5 | 6 | Note: 7 | --------------------------------------------------------------------- 8 | The contents of this file should be added to a AWS Lambda function 9 | created as part of the EDSA Cloud-Computing Predict. 10 | For further guidance around this process, see the README instruction 11 | file which sits at the root of this repo. 12 | --------------------------------------------------------------------- 13 | 14 | """ 15 | 16 | # Find overwhelming sentiment in article 17 | 18 | def find_max_sentiment(Comprehend_Sentiment_Output): 19 | 20 | sentiment_score = 0 21 | 22 | if Comprehend_Sentiment_Output['Sentiment'] == 'POSITIVE': 23 | sentiment_score = Comprehend_Sentiment_Output['SentimentScore']['Positive'] 24 | 25 | elif Comprehend_Sentiment_Output['Sentiment'] == 'NEGATIVE': 26 | sentiment_score = Comprehend_Sentiment_Output['SentimentScore']['Negative'] 27 | 28 | elif Comprehend_Sentiment_Output['Sentiment'] == 'NEUTRAL': 29 | sentiment_score = Comprehend_Sentiment_Output['SentimentScore']['Neutral'] 30 | 31 | else: 32 | sentiment_score = Comprehend_Sentiment_Output['SentimentScore']['Mixed'] 33 | 34 | print(sentiment_score, Comprehend_Sentiment_Output['Sentiment']) 35 | 36 | return Comprehend_Sentiment_Output['Sentiment'], sentiment_score 37 | -------------------------------------------------------------------------------- /student_solution_files/predict_detail_submission_template.csv: -------------------------------------------------------------------------------- 1 | Name,Surname,Website_URL,API_Gateway_URL 2 | Dorah,Explorer,https://sample-website-url.com,https://sample-api-url.com -------------------------------------------------------------------------------- /student_solution_files/send_emails_with_ses.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lambda Function used to send emails via Amazon SES. 3 | 4 | Author: Explore Data Science Academy. 5 | 6 | Note: 7 | --------------------------------------------------------------------- 8 | The contents of this file should be added to a AWS Lambda function 9 | created as part of the EDSA Cloud-Computing Predict. 10 | For further guidance around this process, see the README instruction 11 | file which sits at the root of this repo. 12 | --------------------------------------------------------------------- 13 | 14 | """ 15 | 16 | # Lambda dependencies 17 | import boto3 # Python AWS SDK 18 | import json # Used for handling API-based data. 19 | import base64 # Needed to decode the incoming POST data 20 | from botocore.exceptions import ClientError # Catch errors on client side 21 | 22 | def lambda_handler(event, context): 23 | 24 | # Perform JSON data decoding 25 | body_enc = event['body'] 26 | dec_dict = json.loads(base64.b64decode(body_enc)) 27 | 28 | # Sample text that you would like to email to your recipient 29 | # address from your sender address. 30 | email_text = 'Insert your sample email here' 31 | 32 | # ** SES Functionality ** 33 | 34 | # Replace sender@example.com with your "From" address. 35 | # This address must be verified with Amazon SES. 36 | # --- Insert your code here --- 37 | SENDER = 'sender@example.com' 38 | # ----------------------------- 39 | 40 | # Replace recipient@example.com with a "To" address. If your account 41 | # is still in the sandbox, this address must be verified. 42 | # --- Insert your code here --- 43 | RECIPIENT = 'recipient@example.com' 44 | # ----------------------------- 45 | 46 | 47 | # The subject line for the email. 48 | # --- DO NOT MODIFY THIS CODE --- 49 | SUBJECT = f"Data Science Portfolio Project Website - Hello {dec_dict['name']}" 50 | # ------------------------------- 51 | 52 | # The email body for recipients with non-HTML email clients 53 | BODY_TEXT = (email_text) 54 | 55 | # The character encoding for the email. 56 | CHARSET = "UTF-8" 57 | 58 | # Create a new SES service resource 59 | client = boto3.client('ses') 60 | 61 | # Try to send the email. 62 | try: 63 | #Provide the contents of the email. 64 | ses_response = client.send_email( 65 | Destination={ 66 | 'ToAddresses': [ 67 | RECIPIENT, 68 | # 'edsa.predicts@explore-ai.net', # <--- Uncomment this line once you have successfully tested your predict end-to-end 69 | ], 70 | }, 71 | Message={ 72 | 'Body': { 73 | 74 | 'Text': { 75 | 'Charset': CHARSET, 76 | 'Data': BODY_TEXT, 77 | }, 78 | }, 79 | 'Subject': { 80 | 'Charset': CHARSET, 81 | 'Data': SUBJECT, 82 | }, 83 | }, 84 | Source=SENDER, 85 | ) 86 | 87 | # Display an error if something goes wrong. 88 | except ClientError as e: 89 | print(e.response['Error']['Message']) 90 | else: 91 | print("Email sent! Message ID:"), 92 | print(ses_response['MessageId']) 93 | 94 | # ** Create a response object to inform the website 95 | # that the workflow executed successfully. ** 96 | lambda_response = { 97 | 'statusCode': 200, 98 | 'body': json.dumps({ 99 | 'Name': dec_dict['name'], 100 | 'Email': dec_dict['email'], 101 | 'Cell': dec_dict['phone'], 102 | 'Message': dec_dict['message'], 103 | 'SES_response': ses_response, 104 | 'Email_message': email_text 105 | }) 106 | } 107 | 108 | return lambda_response 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /student_solution_files/write_data_to_dynamodb.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Lambda function that decodes and writes data from your portfolio website to your DynamoDB database. 4 | 5 | Author: Explore Data Science Academy. 6 | 7 | Note: 8 | --------------------------------------------------------------------- 9 | The contents of this file should be added to a AWS Lambda function 10 | created as part of the EDSA Cloud-Computing Predict. 11 | For further guidance around this process, see the README instruction 12 | file which sits at the root of this repo. 13 | --------------------------------------------------------------------- 14 | 15 | """ 16 | 17 | # Lambda dependencies 18 | import boto3 # Python AWS SDK 19 | import json # Used for handling API-based data. 20 | import base64 # Needed to decode the incoming POST data 21 | 22 | def lambda_handler(event, context): 23 | 24 | # Perform JSON data decoding 25 | body_enc = event['body'] 26 | dec_dict = json.loads(base64.b64decode(body_enc)) 27 | 28 | 29 | # --- Write to dynamodb --- 30 | 31 | # ** Create a variable that can take a random value between 1 and 1 000 000 000. 32 | # This variable will be used as our key value i.e the ResponsesID and should be of type integer. 33 | # It is important to note that the ResponseID i.e. the rid variable, should take 34 | # on a unique value to prevent errors when writing to DynamoDB. ** 35 | 36 | # --- Insert your code here --- 37 | rid = None # <--- Replace this value with your code. 38 | # ----------------------------- 39 | 40 | # ** Instantiate the DynamoDB service with the help of the boto3 library ** 41 | 42 | # --- Insert your code here --- 43 | dynamodb = None # <--- Replace this value with your code. 44 | # ----------------------------- 45 | 46 | # Instantiate the table. Remember pass the name of the DynamoDB table created in step 4 47 | table = dynamodb.Table('# Insert the name of your generated DynamoDB table here') 48 | 49 | # ** Write the responses to the table using the put_item method. ** 50 | 51 | # Complete the below code so that the appropriate 52 | # incoming data is sent to the matching column in your DynamoDB table 53 | # --- Insert your code here --- 54 | db_response = table.put_item(Item={'ResponsesID': None, # <--- Insert the correct variable 55 | 'Name': None, # <--- Insert the correct variable 56 | 'Email': None, # <--- Insert the correct variable 57 | 'Cell': None, # <--- Insert the correct variable 58 | 'Message': None # <--- Insert the correct variable 59 | }) 60 | # ----------------------------- 61 | 62 | # ** Create a response object to inform the website 63 | # that the workflow executed successfully. ** 64 | lambda_response = { 65 | 'statusCode': 200, 66 | 'body': json.dumps({ 67 | 'Name': dec_dict['name'], 68 | 'Email': dec_dict['email'], 69 | 'Cell': dec_dict['phone'], 70 | 'Message': dec_dict['message'], 71 | 'DB_response': db_response 72 | }) 73 | } 74 | 75 | return lambda_response 76 | 77 | 78 | 79 | --------------------------------------------------------------------------------