├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYING ├── LICENSE ├── NOTICE ├── README.md ├── THIRD-PARTY-LICENSES ├── _compile_cloudformation_template.py ├── api ├── .coveragerc ├── base.py ├── choice_algorithm.py ├── conftest.py ├── test_base.py ├── test_choice_algorithm.py ├── test_wheel.py ├── test_wheel_participant.py ├── utils.py ├── wheel.py └── wheel_participant.py ├── buildspec.yml ├── cloudformation ├── api_gateway.yml ├── api_gateway_lambda_roles.yml ├── aws-ops-wheel.yml ├── awsopswheel-create-policy.json ├── cognito.yml ├── continuous-deployment.yml ├── lambda.yml └── source-bucket.yml ├── hooks └── pre-push ├── requirements.txt ├── run ├── screenshots ├── participants_table.png ├── wheel_post_spin.png ├── wheel_pre_spin.png └── wheels_table.png ├── ui ├── .babelrc ├── .flowconfig ├── .mocharc.json ├── mocha.opts ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── app.jsx │ │ ├── confirmation_modal.jsx │ │ ├── login.jsx │ │ ├── navigation.jsx │ │ ├── notFound.jsx │ │ ├── participant_table │ │ │ ├── participant_modal.jsx │ │ │ ├── participant_row.jsx │ │ │ └── participant_table.jsx │ │ ├── wheel.jsx │ │ └── wheel_table │ │ │ ├── wheel_modal.jsx │ │ │ ├── wheel_row.jsx │ │ │ └── wheel_table.jsx │ ├── index.html │ ├── index.jsx │ ├── static_content │ │ ├── favicon.ico │ │ └── wheel_click.mp3 │ ├── styles.css │ ├── types.jsx │ └── util.jsx ├── test │ ├── components │ │ ├── confirmation_modal.test.jsx │ │ ├── login.test.jsx │ │ ├── navigation.test.jsx │ │ ├── notfound.test.jsx │ │ ├── participant_modal.test.jsx │ │ ├── participant_row.test.jsx │ │ ├── participant_table.test.jsx │ │ ├── wheel.test.jsx │ │ ├── wheel_modal.test.jsx │ │ ├── wheel_row.test.jsx │ │ └── wheel_table.test.jsx │ ├── globals.jsx │ ├── index.test.jsx │ ├── setup.js │ └── shim_data.js └── webpack.config.js └── utils └── wheel_feeder.py /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Github Issue #, if in response to a github-tracked issue:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | /build 4 | /ui/node_modules 5 | /ui/flow-typed 6 | /ui/development_app_location.js 7 | /ui/.nyc_output 8 | /deploy 9 | .env 10 | .venv 11 | env/ 12 | venv/ 13 | __pycache__ 14 | *.pyc 15 | .coverage 16 | .pytest_cache/ 17 | .vscode 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws/aws-ops-wheel/issues), or [recently closed](https://github.com/aws/aws-ops-wheel/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/aws-ops-wheel/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Licensing 54 | 55 | See the [LICENSE](https://github.com/aws/aws-ops-wheel/blob/master/LICENSE) file for our project's licensing. We will ask you confirm the licensing of your contribution. 56 | 57 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Ops Wheel 2 | 3 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /api/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *test* 4 | -------------------------------------------------------------------------------- /api/base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | import decimal 15 | import functools 16 | import json 17 | import traceback 18 | import os 19 | 20 | 21 | class ClientError(Exception): 22 | status_code = 400 23 | 24 | 25 | class BadRequestError(ClientError): 26 | pass 27 | 28 | 29 | class NotFoundError(ClientError): 30 | status_code = 404 31 | 32 | 33 | class DecimalEncoder(json.JSONEncoder): 34 | def default(self, o): 35 | if isinstance(o, decimal.Decimal): 36 | return float(o) 37 | return super(DecimalEncoder, self).default(o) 38 | 39 | 40 | class Response: 41 | def __init__(self, body=None, headers=None, status_code=None): 42 | self.headers = headers or {} 43 | self.body = body 44 | self.status_code = status_code 45 | 46 | def to_response(self): 47 | response = {'headers': self.headers} 48 | status_code = self.status_code 49 | if self.body is not None: 50 | if status_code is None: 51 | status_code = 200 52 | if 'Content-Type' in response['headers']: 53 | response['body'] = self.body 54 | else: 55 | response['headers']['Content-Type'] = 'application/json' 56 | response['body'] = json.dumps(self.body, cls=DecimalEncoder, sort_keys=True, indent=2) 57 | 58 | if status_code is None: 59 | status_code = 201 60 | response['statusCode'] = status_code 61 | return response 62 | 63 | 64 | class route: 65 | # Shared route registry 66 | registry = {} 67 | 68 | def __init__(self, path, methods): 69 | self.path = path 70 | self.methods = methods 71 | 72 | def __call__(self, func): 73 | """ 74 | Helper for handling exceptions within the lambda function 75 | and returning back appropriate responses 76 | """ 77 | assert func.__name__ not in self.__class__.registry, f"There are 2 routed functions called {func.__name__}" 78 | 79 | @functools.wraps(func) 80 | def wrapper(event, context=None): 81 | try: 82 | if event['body'] is None or isinstance(event['body'], str): 83 | event['body'] = json.loads(event.get('body', None) or '{}') 84 | if not isinstance(event['body'], dict): 85 | raise Exception 86 | except Exception: 87 | return Response(body=f"Malformed JSON: {event['body']}", status_code=400).to_response() 88 | try: 89 | response = func(event) 90 | if not isinstance(response, Response): 91 | response = Response(body=response) 92 | return response.to_response() 93 | except ClientError as e: 94 | return Response(body=str(e), status_code=e.status_code).to_response() 95 | except Exception: 96 | return Response( 97 | body=f"Internal Service Exception: {traceback.format_exc()}", 98 | status_code=500 99 | ).to_response() 100 | wrapper.route = self 101 | self.__class__.registry[func.__name__] = wrapper 102 | setattr(self.__class__, func.__name__, wrapper) 103 | return wrapper 104 | 105 | 106 | @route('/config', methods=['GET']) 107 | def config(event): 108 | return { 109 | 'USER_POOL_ID': os.environ.get('USER_POOL_ID', None), 110 | 'APP_CLIENT_ID': os.environ.get('APP_CLIENT_ID', None), 111 | } 112 | -------------------------------------------------------------------------------- /api/choice_algorithm.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | from boto3.dynamodb.conditions import Key 15 | from utils import Wheel, WheelParticipant, to_update_kwargs 16 | from base import BadRequestError 17 | import random 18 | import contextlib 19 | from decimal import Decimal 20 | 21 | 22 | def suggest_participant(wheel): 23 | """ 24 | Suggest a participant given weights of all participants with randomization. 25 | This is weighted selection where all participants start with a weight of 1, 26 | so the sum of the weights will always equal the number of participants 27 | :param wheel: Wheel dictionary: 28 | { 29 | "id": string ID of the wheel (DDB Hash Key), 30 | "name": string name of the wheel, 31 | "participant_count": number of participants in the wheel, 32 | } 33 | :return: ID of the suggested participant 34 | """ 35 | if wheel['participant_count'] == 0: 36 | raise BadRequestError("Cannot suggest a participant when the wheel doesn't have any!") 37 | 38 | query_params = {'KeyConditionExpression': Key('wheel_id').eq(wheel['id'])} 39 | 40 | participants = WheelParticipant.iter_query(**query_params) 41 | selected_total_weight = random.random() * float(sum([participant['weight'] for participant in participants])) 42 | 43 | # We do potentially want to return the last participant just as a safeguard for rounding errors 44 | participant = None 45 | for participant in WheelParticipant.iter_query(**query_params): 46 | selected_total_weight -= float(participant['weight']) 47 | if selected_total_weight <= 0: 48 | return participant['id'] 49 | return participant['id'] 50 | 51 | 52 | def select_participant(wheel, participant): 53 | """ 54 | Register the selection of a participant by updating the weights of all participants for a given wheel 55 | :param wheel: Wheel dictionary: 56 | { 57 | "id": string ID of the wheel (DDB Hash Key), 58 | "name": string name of the wheel, 59 | "participant_count": number of participants in the wheel, 60 | } 61 | :param participant: Participant dictionary: 62 | { 63 | "id": string ID of the participant (DDB Hash Key), 64 | "name": string name of the participant, 65 | "url": Participant's URL, 66 | "wheel_id": string ID of the wheel the participant belongs to, 67 | "weight": participant's weight in the selection algorithm 68 | } 69 | :return: None 70 | """ 71 | 72 | num_participants = 0 73 | total_weight = Decimal(0) 74 | for p in WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): 75 | num_participants = num_participants+1 76 | total_weight += p['weight'] 77 | 78 | # Factor is the number by which all weights must be multiplied 79 | # so total weight will be equal to the number of participants. 80 | factor = Decimal(num_participants) / total_weight 81 | 82 | if num_participants > 1: 83 | weight_share = participant['weight'] / Decimal(num_participants - 1) 84 | with WheelParticipant.batch_writer() as batch: 85 | # Redistribute and normalize the weights. 86 | for p in WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): 87 | if p['id'] == participant['id']: 88 | p['weight'] = 0 89 | else: 90 | p['weight'] += Decimal(weight_share) 91 | p['weight'] *= factor 92 | batch.put_item(Item=p) 93 | Wheel.update_item( 94 | Key={'id': wheel['id']}, 95 | **to_update_kwargs({'participant_count': num_participants}) 96 | ) 97 | 98 | def reset_wheel(wheel): 99 | """ 100 | Resets the weights of all participants in the wheel and updates the wheel's participant count 101 | :param wheel: Wheel dictionary: 102 | { 103 | "id": string ID of the wheel (DDB Hash Key), 104 | "name": string name of the wheel, 105 | "participant_count": number of participants in the wheel, 106 | } 107 | :return: None 108 | """ 109 | count = 0 110 | with WheelParticipant.batch_writer() as batch: 111 | for p in WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): 112 | p['weight'] = get_sub_wheel_size(p['name']) 113 | batch.put_item(Item=p) 114 | count += 1 115 | Wheel.update_item(Key={'id': wheel['id']}, **to_update_kwargs({'participant_count': count})) 116 | 117 | def get_sub_wheel_size(wheel_name): 118 | resp = Wheel.query( 119 | IndexName='name_index', 120 | KeyConditionExpression=Key('name').eq(wheel_name) 121 | ) 122 | if len(resp['Items']): # if a matching wheel is found 123 | return int(resp['Items'][0]['participant_count']) or 1 # if wheel size is 0, default to 1 124 | return 1 # default to 1 if no matching wheel is found 125 | 126 | 127 | @contextlib.contextmanager 128 | def wrap_wheel_creation(wheel): 129 | wheel['participant_count'] = 0 130 | yield 131 | 132 | 133 | @contextlib.contextmanager 134 | def wrap_participant_creation(wheel, participant): 135 | participant['weight'] = get_sub_wheel_size(participant['name']) 136 | yield 137 | count = 0 138 | with WheelParticipant.batch_writer() as batch: 139 | for p in WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): 140 | count += 1 141 | Wheel.update_item( 142 | Key={'id': wheel['id']}, 143 | **to_update_kwargs({'participant_count': count}) 144 | ) 145 | 146 | 147 | def on_participant_deletion(wheel, participant): 148 | """ 149 | Normalize the remaining participant weights to account for participant removal. 150 | The ratio is based on the following: 151 | 1) The participant should be at weight=1 when it leaves the system (which is the same as it arrived) 152 | 2) That difference should be split by the remaining participants proportional by weight 153 | This ensures that 'weight=0' participants are still at weight=0 and that the sum of all 154 | weights is equal to the number of participants, so new additions are treated fairly 155 | :param wheel: Wheel dictionary: 156 | { 157 | "id": string ID of the wheel (DDB Hash Key), 158 | "name": string name of the wheel, 159 | "participant_count": number of participants in the wheel, 160 | } 161 | :param participant: Participant dictionary: 162 | { 163 | "id": string ID of the wheel (DDB Hash Key), 164 | "name": string name of the wheel, 165 | "url": Participant's URL, 166 | "wheel_id": string ID of the wheel the participant belongs to, 167 | } 168 | :return: None 169 | """ 170 | total_weight = participant['weight'] 171 | for p in WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): 172 | total_weight += p['weight'] 173 | 174 | weight = participant['weight'] 175 | remaining_weight = total_weight - weight # <-- no longer presumes existing weight balance via 'int(participant_count)' 176 | ratio = (1 + ((weight - 1) / remaining_weight)) if (remaining_weight != 0) else 1 177 | num_participants = Decimal(0) 178 | with WheelParticipant.batch_writer() as batch: 179 | for p in WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): 180 | if p['id'] != participant['id']: 181 | # This is cast to a string before turning into a decimal because of rounding/inexact guards in boto3 182 | p['weight'] = Decimal(str(float(p['weight']) * float(ratio))) if (remaining_weight != 0) else \ 183 | get_sub_wheel_size(p['name']) 184 | batch.put_item(Item=p) 185 | num_participants = num_participants+1 186 | 187 | Wheel.update_item( 188 | Key={'id': wheel['id']}, 189 | **to_update_kwargs({'participant_count': num_participants}) 190 | ) 191 | -------------------------------------------------------------------------------- /api/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | import os 15 | import pytest 16 | from boto3.session import Session 17 | from moto import mock_dynamodb as ddb_mock 18 | with ddb_mock(): 19 | from utils import add_extended_table_functions 20 | import utils 21 | 22 | WHEEL_TABLE_NAME = os.environ.get('WHEEL_TABLE', 'DevOpsWheel-Wheels') 23 | PARTICIPANT_TABLE_NAME = os.environ.get('PARTICIPANT_TABLE', 'DevOpsWheel-Participants') 24 | 25 | 26 | @pytest.fixture(scope='session') 27 | def mock_dynamodb(): 28 | 29 | ddb_mock().start() 30 | 31 | session = Session(aws_access_key_id='', aws_secret_access_key='') 32 | dynamodb = session.resource('dynamodb') 33 | 34 | wheel_table = dynamodb.create_table( 35 | TableName=WHEEL_TABLE_NAME, 36 | KeySchema=[ 37 | { 38 | 'AttributeName': 'id', 39 | 'KeyType': 'HASH' 40 | } 41 | ], 42 | AttributeDefinitions=[ 43 | { 44 | 'AttributeName': 'id', 45 | 'AttributeType': 'S' 46 | }, 47 | { 48 | 'AttributeName': 'name', 49 | 'AttributeType': 'S' 50 | } 51 | ], 52 | BillingMode='PAY_PER_REQUEST', 53 | GlobalSecondaryIndexes=[{ 54 | 'IndexName': 'name_index', 55 | 'KeySchema': [{ 56 | 'AttributeName': 'name', 57 | 'KeyType': 'HASH' 58 | }], 59 | 'Projection': { 60 | 'ProjectionType': 'ALL' 61 | } 62 | }] 63 | ) 64 | 65 | participant_table = dynamodb.create_table( 66 | TableName=PARTICIPANT_TABLE_NAME, 67 | KeySchema=[ 68 | { 69 | 'AttributeName': 'wheel_id', 70 | 'KeyType': 'HASH' 71 | }, 72 | { 73 | 'AttributeName': 'id', 74 | 'KeyType': 'RANGE' 75 | } 76 | ], 77 | AttributeDefinitions=[ 78 | { 79 | 'AttributeName': 'wheel_id', 80 | 'AttributeType': 'S' 81 | }, 82 | { 83 | 'AttributeName': 'id', 84 | 'AttributeType': 'S' 85 | } 86 | ], 87 | BillingMode='PAY_PER_REQUEST' 88 | ) 89 | 90 | # Wait on table creation 91 | wheel_table.meta.client.get_waiter('table_exists').wait(TableName=WHEEL_TABLE_NAME) 92 | participant_table.meta.client.get_waiter('table_exists').wait(TableName=PARTICIPANT_TABLE_NAME) 93 | 94 | yield dynamodb 95 | 96 | ddb_mock().stop() 97 | 98 | 99 | @pytest.fixture 100 | def mock_wheel_table(mock_dynamodb): 101 | Wheel = mock_dynamodb.Table(WHEEL_TABLE_NAME) 102 | add_extended_table_functions(Wheel) 103 | utils.Wheel = Wheel 104 | yield Wheel 105 | wheels = Wheel.scan()['Items'] 106 | with Wheel.batch_writer() as batch: 107 | for wheel in wheels: 108 | batch.delete_item(Key={'id': wheel['id']}) 109 | 110 | 111 | @pytest.fixture 112 | def mock_participant_table(mock_dynamodb): 113 | WheelParticipant = mock_dynamodb.Table(PARTICIPANT_TABLE_NAME) 114 | add_extended_table_functions(WheelParticipant) 115 | utils.WheelParticipant = WheelParticipant 116 | yield WheelParticipant 117 | participants = WheelParticipant.scan()['Items'] 118 | with WheelParticipant.batch_writer() as batch: 119 | for participant in participants: 120 | batch.delete_item(Key={'id': participant['id'], 'wheel_id': participant['wheel_id']}) 121 | -------------------------------------------------------------------------------- /api/test_base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | import pytest 15 | import wheel 16 | from moto import mock_dynamodb 17 | 18 | 19 | @mock_dynamodb 20 | def test_no_dynamodb_available(): 21 | response = wheel.create_wheel({'body': {'name': 'DDB not available'}}) 22 | assert response['statusCode'] == 500 23 | 24 | 25 | def test_missing_body(mock_dynamodb): 26 | with pytest.raises(Exception): 27 | wheel.create_wheel({'not_body': 'Nobody is in here'}) 28 | -------------------------------------------------------------------------------- /api/test_choice_algorithm.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | import pytest 15 | import json 16 | import choice_algorithm 17 | import wheel 18 | import wheel_participant 19 | 20 | from decimal import Decimal 21 | from utils import Wheel, WheelParticipant, to_update_kwargs 22 | from boto3.dynamodb.conditions import Key 23 | from base import BadRequestError 24 | import random 25 | 26 | epsilon = 1E-6 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | def setup_data(mock_dynamodb): 31 | names = ['Dan', 'Bob', 'Steve', 'Jerry', 'Frank', 'Alexa', 'Jeff'] 32 | 33 | created_wheel = json.loads(wheel.create_wheel({'body': {'name': 'Test Wheel'}})['body']) 34 | 35 | create_participant_events = [{ 36 | 'pathParameters': { 37 | 'wheel_id': created_wheel['id'] 38 | }, 39 | 'body': { 40 | 'name': name, 41 | 'url': 'https://amazon.com' 42 | } 43 | } for name in names] 44 | 45 | created_participants = [json.loads(wheel_participant.create_participant(event)['body']) for event in 46 | create_participant_events] 47 | 48 | # Reloads the wheel with updated participant count 49 | return { 50 | 'wheel': json.loads(wheel.get_wheel({'body': {}, 'pathParameters': {'wheel_id': created_wheel['id']}})['body']), 51 | 'participants': created_participants 52 | } 53 | 54 | 55 | def test_suggest_participant(mock_dynamodb, setup_data): 56 | participant_ids = [participant['id'] for participant in setup_data['participants']] 57 | assert choice_algorithm.suggest_participant(setup_data['wheel']) in participant_ids 58 | 59 | 60 | def test_suggest_participant_no_participants(mock_dynamodb): 61 | wheel = {'participant_count': 0} 62 | with pytest.raises(BadRequestError): 63 | choice_algorithm.suggest_participant(wheel) 64 | 65 | 66 | def test_select_participant(mock_dynamodb, setup_data, mock_participant_table): 67 | participant_to_select = setup_data['participants'][0] 68 | choice_algorithm.select_participant(setup_data['wheel'], participant_to_select) 69 | 70 | participants = mock_participant_table.query( 71 | KeyConditionExpression=Key('wheel_id').eq(setup_data['wheel']['id']))['Items'] 72 | selected_participant = [participant for participant in participants 73 | if participant['id'] == participant_to_select['id']][0] 74 | 75 | assert selected_participant['weight'] == 0 76 | assert abs(sum([participant['weight'] for participant in participants]) - len(participants)) < epsilon 77 | 78 | 79 | def test_selection_cycle(mock_dynamodb, setup_data, mock_participant_table): 80 | def get_participant_with_id(participants, target_id): 81 | for p in participants: 82 | if p['id'] == target_id: 83 | return p 84 | return None 85 | 86 | rngstate = random.getstate() 87 | random.seed(0) # Make the (otherwise pseudorandom) test repeatable. 88 | 89 | participants = WheelParticipant.scan()['Items'] 90 | wheel = setup_data['wheel'] 91 | total_weight_of_chosens = 0 92 | num_iterations = 200 93 | 94 | distro = {} 95 | for participant in participants: 96 | distro[participant['name']] = 0 97 | 98 | for _ in range(0, num_iterations): 99 | 100 | chosen_id = choice_algorithm.suggest_participant(wheel) 101 | 102 | chosen_was = get_participant_with_id(participants, chosen_id) 103 | chosen_was_weight = chosen_was['weight'] 104 | 105 | distro[chosen_was['name']] = distro[chosen_was['name']] + 1 106 | 107 | choice_algorithm.select_participant(wheel, chosen_was) 108 | 109 | participants = WheelParticipant.scan()['Items'] 110 | 111 | chosen_now = get_participant_with_id(participants, chosen_id) 112 | chosen_now_weight = chosen_now['weight'] 113 | 114 | assert chosen_was_weight > 0.0 115 | assert chosen_now_weight == 0 116 | total_weight_of_chosens += chosen_was_weight 117 | 118 | total_weight = sum([participant['weight'] for participant in participants]) 119 | assert abs(total_weight - len(participants)) < epsilon 120 | 121 | # Must match human-inspected reasonable values for the RNG seed defined 122 | # above for number of times each participant was chosen, and the total 123 | # weight of participants selected. These are a rough equivalent to 124 | # ensuring that the sequence of chosen participants matches the observed 125 | # test run. 126 | dv = list(distro.values()) 127 | list.sort(dv) 128 | human_observed_selection_counts = [26, 27, 27, 28, 29, 30, 33] 129 | human_observed_total_weight = 323.15697757934635 130 | assert dv == human_observed_selection_counts 131 | assert abs(float(total_weight_of_chosens) - human_observed_total_weight) < epsilon 132 | 133 | # Put things back the way they were. 134 | random.setstate(rngstate) 135 | 136 | 137 | def test_reset_wheel(mock_dynamodb, setup_data, mock_participant_table): 138 | choice_algorithm.select_participant(setup_data['wheel'], setup_data['participants'][0]) 139 | choice_algorithm.reset_wheel(setup_data['wheel']) 140 | 141 | updated_participants = mock_participant_table.query( 142 | KeyConditionExpression=Key('wheel_id').eq(setup_data['wheel']['id']))['Items'] 143 | participant_weights = [participant['weight'] for participant in updated_participants] 144 | 145 | for weight in participant_weights: 146 | assert weight == 1 147 | 148 | 149 | def test_rebalance_wheel(setup_data, mock_participant_table): 150 | def set_up_test(setup_data, mock_participant_table): 151 | # Select a participant to take everyone off their 1.0 scores. 152 | choice_algorithm.select_participant(setup_data['wheel'], setup_data['participants'][0]) 153 | 154 | # Adjust participants to different weights to take the wheel out of balance. 155 | participants = mock_participant_table.query( 156 | KeyConditionExpression=Key('wheel_id').eq(setup_data['wheel']['id']))['Items'] 157 | with WheelParticipant.batch_writer() as batch: 158 | for p in participants: 159 | p['weight'] += Decimal(.15) 160 | batch.put_item(Item=p) 161 | 162 | # Confirm that the wheel is out of balance. 163 | participants = mock_participant_table.query( 164 | KeyConditionExpression=Key('wheel_id').eq(setup_data['wheel']['id']))['Items'] 165 | participant_weights = [participant['weight'] for participant in participants] 166 | 167 | total_weight = Decimal(0) 168 | for weight in participant_weights: 169 | total_weight += weight 170 | assert abs(total_weight-Decimal(8.05)) < epsilon 171 | 172 | def complete_test(setup_data, mock_participant_table): 173 | # Confirm that rebalancing has taken place. 174 | participants = mock_participant_table.query( 175 | KeyConditionExpression=Key('wheel_id').eq(setup_data['wheel']['id']))['Items'] 176 | 177 | participant_weights = [participant['weight'] for participant in participants] 178 | 179 | total_weight = Decimal(0) 180 | for weight in participant_weights: 181 | total_weight += weight 182 | 183 | assert abs(total_weight-len(participants)) < epsilon 184 | 185 | set_up_test(setup_data, mock_participant_table) 186 | # Select a participant to cause rebalancing to take place. 187 | participants = mock_participant_table.query( 188 | KeyConditionExpression=Key('wheel_id').eq(setup_data['wheel']['id']))['Items'] 189 | choice_algorithm.select_participant(setup_data['wheel'], participants[3]) 190 | complete_test(setup_data, mock_participant_table) 191 | 192 | 193 | def test_fix_incorrect_participant_count(mock_dynamodb, setup_data, mock_wheel_table): 194 | out_of_whack = 999 195 | wheel = setup_data['wheel'] 196 | wheel_id = wheel['id'] 197 | proper_participant_count = wheel['participant_count'] 198 | 199 | # # # # We will first test this on a select_participant operation. 200 | 201 | # Throw the participant count way out of whack. 202 | mock_wheel_table.update_item( 203 | Key={'id': wheel['id']}, 204 | **to_update_kwargs({'participant_count': out_of_whack}) 205 | ) 206 | 207 | participant_count = mock_wheel_table.query( 208 | KeyConditionExpression=Key('id').eq(wheel['id']))['Items'][0].get('participant_count') 209 | 210 | # # Ensure it's out of whack. 211 | assert abs(out_of_whack - participant_count) < epsilon 212 | 213 | # Select a participant to cause correction of participant count. 214 | wheel = Wheel.get_existing_item(Key={'id': wheel_id}) 215 | choice_algorithm.select_participant(wheel, setup_data['participants'][0]) 216 | 217 | # ...and ensure it's back into whack. 218 | participant_count = mock_wheel_table.query( 219 | KeyConditionExpression=Key('id').eq(wheel['id']))['Items'][0].get('participant_count') 220 | 221 | assert abs(Decimal(proper_participant_count) - participant_count) < epsilon 222 | 223 | # # # # We will next test this on a delete_participant operation. 224 | 225 | # Throw the participant count way out of whack. 226 | mock_wheel_table.update_item( 227 | Key={'id': wheel['id']}, 228 | **to_update_kwargs({'participant_count': out_of_whack}) 229 | ) 230 | 231 | participant_count = mock_wheel_table.query( 232 | KeyConditionExpression=Key('id').eq(wheel['id']))['Items'][0].get('participant_count') 233 | 234 | # # Ensure it's out of whack. 235 | assert abs(out_of_whack - participant_count) < epsilon 236 | 237 | # Delete a participant to cause correction of participant count. 238 | event = {'body': {}, 'pathParameters': {'wheel_id': wheel_id, 'participant_id': setup_data['participants'][0]['id']}} 239 | wheel_participant.delete_participant(event) 240 | 241 | # # ...and ensure it's back into whack. 242 | participant_count = mock_wheel_table.query( 243 | KeyConditionExpression=Key('id').eq(wheel['id']))['Items'][0].get('participant_count') 244 | 245 | assert abs((Decimal(proper_participant_count)-1) - participant_count) < epsilon 246 | 247 | # # # # We will next test this on a create_participant operation. 248 | 249 | # Throw the participant count way out of whack. 250 | mock_wheel_table.update_item( 251 | Key={'id': wheel['id']}, 252 | **to_update_kwargs({'participant_count': out_of_whack}) 253 | ) 254 | 255 | participant_count = mock_wheel_table.query( 256 | KeyConditionExpression=Key('id').eq(wheel['id']))['Items'][0].get('participant_count') 257 | 258 | # # Ensure it's out of whack. 259 | assert abs(out_of_whack - participant_count) < epsilon 260 | 261 | # Add a participant to cause correction of participant count. 262 | event = {'pathParameters': {'wheel_id': wheel_id},'body': {'name': 'Ishmael-on-the-Sea','url': 'https://amazon.com'}} 263 | wheel_participant.create_participant(event) 264 | 265 | # # ...and ensure it's back into whack. 266 | participant_count = mock_wheel_table.query( 267 | KeyConditionExpression=Key('id').eq(wheel['id']))['Items'][0].get('participant_count') 268 | 269 | assert abs((Decimal(proper_participant_count)) - participant_count) < epsilon 270 | -------------------------------------------------------------------------------- /api/test_wheel.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | import pytest 15 | import json 16 | import wheel 17 | from utils import get_uuid 18 | from base import NotFoundError 19 | 20 | 21 | def test_create_wheel(mock_dynamodb, mock_wheel_table): 22 | event = {'body': {'name': 'Test Wheel'}} 23 | 24 | response = wheel.create_wheel(event) 25 | created_wheel = json.loads(response['body']) 26 | 27 | assert response['statusCode'] == 200 28 | assert created_wheel['name'] == event['body']['name'] 29 | assert mock_wheel_table.get_existing_item(Key={'id': created_wheel['id']}) 30 | 31 | 32 | def test_invalid_create_wheel(mock_dynamodb): 33 | response = wheel.create_wheel({'body': {'name': ''}}) 34 | 35 | assert response['statusCode'] == 400 36 | assert 'New wheels require a name that must be a string with a length of at least 1' in response['body'] 37 | 38 | 39 | def test_delete_wheel(mock_dynamodb, mock_participant_table, mock_wheel_table): 40 | test_wheel = {'id': get_uuid()} 41 | participant = {'id': get_uuid(), 'wheel_id': test_wheel['id']} 42 | 43 | mock_wheel_table.put_item(Item=test_wheel) 44 | mock_participant_table.put_item(Item=participant) 45 | 46 | event = {'body': {}, 'pathParameters': {'wheel_id': test_wheel['id']}} 47 | response = wheel.delete_wheel(event) 48 | 49 | assert response['statusCode'] == 201 50 | with pytest.raises(NotFoundError): 51 | mock_wheel_table.get_existing_item(Key=test_wheel) 52 | with pytest.raises(NotFoundError): 53 | mock_participant_table.get_existing_item(Key=participant) 54 | 55 | 56 | def test_get_wheel(mock_dynamodb, mock_wheel_table): 57 | test_wheel = { 58 | 'id': get_uuid(), 59 | 'name': 'Test Wheel' 60 | } 61 | mock_wheel_table.put_item(Item=test_wheel) 62 | 63 | event = {'body': {}, 'pathParameters': {'wheel_id': test_wheel['id']}} 64 | response = wheel.get_wheel(event) 65 | 66 | assert response['statusCode'] == 200 67 | assert json.loads(response['body']) == test_wheel 68 | 69 | 70 | def test_list_wheels(mock_dynamodb, mock_wheel_table): 71 | test_wheels = [{ 72 | 'id': get_uuid(), 73 | 'name': 'Wheel ' + num 74 | } for num in ['0', '1']] 75 | 76 | with mock_wheel_table.batch_writer() as batch: 77 | for test_wheel in test_wheels: 78 | batch.put_item(Item=test_wheel) 79 | 80 | response = wheel.list_wheels({'body': {}}) 81 | 82 | assert response['statusCode'] == 200 83 | assert 'Wheel 0' in response['body'] and 'Wheel 1' in response['body'] 84 | assert json.loads(response['body'])['Count'] == len(test_wheels) 85 | 86 | 87 | def test_update_wheel(mock_dynamodb, mock_wheel_table): 88 | test_wheel = { 89 | 'id': get_uuid(), 90 | 'name': 'Old Wheel Name', 91 | } 92 | 93 | mock_wheel_table.put_item(Item=test_wheel) 94 | 95 | new_name = 'New Wheel Name' 96 | event = {'body': {'name': new_name}, 'pathParameters': {'wheel_id': test_wheel['id']}} 97 | response = wheel.update_wheel(event) 98 | 99 | assert response['statusCode'] == 200 100 | assert json.loads(response['body'])['name'] == new_name 101 | 102 | 103 | def test_invalid_update_wheel(mock_dynamodb, mock_wheel_table): 104 | test_wheel = { 105 | 'id': get_uuid(), 106 | 'name': 'Old Wheel Name', 107 | } 108 | 109 | mock_wheel_table.put_item(Item=test_wheel) 110 | 111 | event = {'body': {'name': ''}, 'pathParameters': {'wheel_id': test_wheel['id']}} 112 | response = wheel.update_wheel(event) 113 | 114 | assert response['statusCode'] == 400 115 | assert 'Updating a wheel requires a new name of at least 1 character in length' in response['body'] 116 | 117 | 118 | def test_unrig_participant(mock_dynamodb, mock_wheel_table): 119 | test_wheel = { 120 | 'id': get_uuid(), 121 | 'name': 'Test Wheel', 122 | 'rigging': { 123 | 'participant_id': get_uuid(), 124 | 'hidden': False 125 | } 126 | } 127 | 128 | mock_wheel_table.put_item(Item=test_wheel) 129 | 130 | event = {'body': {}, 'pathParameters': {'wheel_id': test_wheel['id']}} 131 | response = wheel.unrig_participant(event) 132 | 133 | assert response['statusCode'] == 201 134 | assert 'rigging' not in mock_wheel_table.get_existing_item(Key={'id': test_wheel['id']}) 135 | -------------------------------------------------------------------------------- /api/test_wheel_participant.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | import pytest 15 | import json 16 | import wheel_participant 17 | from utils import get_uuid, to_update_kwargs 18 | from base import NotFoundError 19 | 20 | WHEEL_ID = get_uuid() 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def setup_wheel(mock_dynamodb, mock_wheel_table): 25 | wheel = { 26 | 'id': WHEEL_ID, 27 | 'name': 'Test Participant API Wheel', 28 | 'participant_count': 0, 29 | } 30 | mock_wheel_table.put_item(Item=wheel) 31 | 32 | 33 | def test_create_participant(mock_dynamodb, mock_participant_table): 34 | event = { 35 | 'pathParameters': { 36 | 'wheel_id': WHEEL_ID 37 | }, 38 | 'body': { 39 | 'name': 'Dan', 40 | 'url': 'https://amazon.com' 41 | } 42 | } 43 | 44 | response = wheel_participant.create_participant(event) 45 | created_participant = json.loads(response['body']) 46 | 47 | assert response['statusCode'] == 200 48 | assert created_participant['name'] == event['body']['name'] 49 | assert created_participant['url'] == event['body']['url'] 50 | assert mock_participant_table.get_existing_item(Key={'id': created_participant['id'], 'wheel_id': WHEEL_ID}) 51 | 52 | 53 | def test_invalid_create_participant(mock_dynamodb): 54 | response = wheel_participant.create_participant({ 55 | 'body': { 56 | 'name': '', 'url': '' 57 | }, 58 | 'pathParameters': { 59 | 'wheel_id': WHEEL_ID} 60 | }) 61 | 62 | assert response['statusCode'] == 400 63 | assert 'Participants require a name and url which must be at least 1 character in length' in response['body'] 64 | 65 | 66 | def test_delete_participant(mock_dynamodb, mock_participant_table): 67 | participants = [{ 68 | 'id': get_uuid(), 69 | 'wheel_id': WHEEL_ID, 70 | 'name': name, 71 | 'url': 'https://amazon.com', 72 | 'weight': 1 73 | } for name in ['Dan', 'Alexa']] 74 | 75 | with mock_participant_table.batch_writer() as batch: 76 | for participant in participants: 77 | batch.put_item(Item=participant) 78 | 79 | event = {'body': {}, 'pathParameters': {'wheel_id': WHEEL_ID, 'participant_id': participants[0]['id']}} 80 | response = wheel_participant.delete_participant(event) 81 | 82 | assert response['statusCode'] == 201 83 | with pytest.raises(NotFoundError): 84 | mock_participant_table.get_existing_item(Key={'id': participants[0]['id'], 'wheel_id': WHEEL_ID}) 85 | 86 | 87 | def test_list_participants(mock_dynamodb, mock_participant_table): 88 | participants = [{ 89 | 'id': get_uuid(), 90 | 'wheel_id': WHEEL_ID, 91 | 'name': name, 92 | } for name in ['Dan', 'Alexa']] 93 | 94 | with mock_participant_table.batch_writer() as batch: 95 | for participant in participants: 96 | batch.put_item(Item=participant) 97 | 98 | response = wheel_participant.list_participants({'body': {}, 'pathParameters': {'wheel_id': WHEEL_ID}}) 99 | 100 | assert response['statusCode'] == 200 101 | assert 'Dan' in response['body'] and 'Alexa' in response['body'] 102 | assert len(json.loads(response['body'])) == len(participants) 103 | 104 | 105 | def test_update_participant(mock_dynamodb, mock_participant_table): 106 | participant = { 107 | 'id': get_uuid(), 108 | 'wheel_id': WHEEL_ID, 109 | 'name': 'Old Name', 110 | 'url': 'https://amazon.com', 111 | 'weight': 1 112 | } 113 | mock_participant_table.put_item(Item=participant) 114 | 115 | event = { 116 | 'pathParameters': { 117 | 'wheel_id': WHEEL_ID, 118 | 'participant_id': participant['id'] 119 | }, 120 | 'body': { 121 | 'name': 'New Name', 122 | 'url': 'https://new-website.com' 123 | } 124 | } 125 | response = wheel_participant.update_participant(event) 126 | updated_participant = json.loads(response['body']) 127 | 128 | assert response['statusCode'] == 200 129 | assert updated_participant['name'] == event['body']['name'] 130 | assert updated_participant['url'] == event['body']['url'] 131 | 132 | 133 | def test_invalid_update_participant(mock_dynamodb, mock_participant_table): 134 | participant = { 135 | 'id': get_uuid(), 136 | 'wheel_id': WHEEL_ID, 137 | 'name': 'Old Name', 138 | 'url': 'https://amazon.com', 139 | 'weight': 1 140 | } 141 | mock_participant_table.put_item(Item=participant) 142 | 143 | event = { 144 | 'pathParameters': { 145 | 'wheel_id': WHEEL_ID, 146 | 'participant_id': participant['id'] 147 | }, 148 | 'body': { 149 | 'name': '', 150 | 'url': '' 151 | } 152 | } 153 | response = wheel_participant.update_participant(event) 154 | 155 | assert response['statusCode'] == 400 156 | assert 'Participants names and urls must be at least 1 character in length' in response['body'] 157 | 158 | 159 | def test_select_participant_removes_rigging(mock_dynamodb, mock_participant_table, mock_wheel_table): 160 | mock_wheel_table.update_item(Key={'id': WHEEL_ID}, **to_update_kwargs({'rigging': {}})) 161 | 162 | participant = { 163 | 'id': get_uuid(), 164 | 'wheel_id': WHEEL_ID, 165 | 'name': 'Pick me!', 166 | 'url': 'https://amazon.com', 167 | 'weight': 1 168 | } 169 | mock_participant_table.put_item(Item=participant) 170 | 171 | event = {'body': {}, 'pathParameters': {'wheel_id': WHEEL_ID, 'participant_id': participant['id']}} 172 | response = wheel_participant.select_participant(event) 173 | 174 | assert response['statusCode'] == 201 175 | assert 'rigging' not in mock_wheel_table.get_existing_item(Key={'id': WHEEL_ID}) 176 | 177 | 178 | def test_rig_participant(mock_dynamodb, mock_wheel_table): 179 | event = { 180 | 'body': {'hidden': True}, 181 | 'pathParameters': { 182 | 'wheel_id': WHEEL_ID, 183 | 'participant_id': get_uuid() 184 | } 185 | } 186 | response = wheel_participant.rig_participant(event) 187 | 188 | assert response['statusCode'] == 201 189 | assert 'rigging' in mock_wheel_table.get_existing_item(Key={'id': WHEEL_ID}) 190 | 191 | 192 | def test_suggest_participant_comical_rig(mock_dynamodb, mock_participant_table, mock_wheel_table): 193 | participants = [{ 194 | 'id': get_uuid(), 195 | 'wheel_id': WHEEL_ID, 196 | 'name': name, 197 | } for name in ['Rig me!', 'I cannot win!']] 198 | 199 | with mock_participant_table.batch_writer() as batch: 200 | for participant in participants: 201 | batch.put_item(Item=participant) 202 | mock_wheel_table.update_item(Key={'id': WHEEL_ID}, **to_update_kwargs({ 203 | 'rigging': { 204 | 'hidden': False, 205 | 'participant_id': participants[0]['id'] 206 | } 207 | })) 208 | 209 | response = wheel_participant.suggest_participant({'body': {}, 'pathParameters': {'wheel_id': WHEEL_ID}}) 210 | body = json.loads(response['body']) 211 | assert response['statusCode'] == 200 212 | assert body['participant_id'] == participants[0]['id'] 213 | assert 'rigged' in body 214 | 215 | 216 | def test_suggest_participant_hidden_rig(mock_dynamodb, mock_participant_table, mock_wheel_table): 217 | participants = [{ 218 | 'id': get_uuid(), 219 | 'wheel_id': WHEEL_ID, 220 | 'name': name, 221 | } for name in ['Rig me!', 'I cannot win!']] 222 | 223 | with mock_participant_table.batch_writer() as batch: 224 | for participant in participants: 225 | batch.put_item(Item=participant) 226 | mock_wheel_table.update_item(Key={'id': WHEEL_ID}, **to_update_kwargs({ 227 | 'rigging': { 228 | 'hidden': True, 229 | 'participant_id': participants[0]['id'] 230 | } 231 | })) 232 | 233 | response = wheel_participant.suggest_participant({'body': {}, 'pathParameters': {'wheel_id': WHEEL_ID}}) 234 | body = json.loads(response['body']) 235 | assert response['statusCode'] == 200 236 | assert body['participant_id'] == participants[0]['id'] 237 | assert 'rigged' not in body 238 | -------------------------------------------------------------------------------- /api/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | import boto3 15 | import datetime 16 | import os 17 | import uuid 18 | from base import NotFoundError 19 | 20 | 21 | dynamodb = boto3.resource('dynamodb') 22 | Wheel = dynamodb.Table(os.environ.get('WHEEL_TABLE', 'DevOpsWheel-Wheels')) 23 | WheelParticipant = dynamodb.Table(os.environ.get('PARTICIPANT_TABLE', 'DevOpsWheel-Participants')) 24 | 25 | 26 | def add_extended_table_functions(table): 27 | def get_existing_item(Key, *args, **kwargs): 28 | """ 29 | Add a new 'get_existing_item' method for our tables that will throw a 404 when it doesn't exist 30 | """ 31 | response = table.get_item(Key=Key, *args, **kwargs) 32 | if 'Item' not in response: 33 | raise NotFoundError(f"{table.name} : {Key} Could Not Be Found") 34 | return response['Item'] 35 | 36 | def iter_query(*args, **kwargs): 37 | """Unwrap pagination from DynamoDB query results to yield items""" 38 | query_results = None 39 | while query_results is None or 'LastEvaluatedKey' in query_results: 40 | if query_results is not None: 41 | kwargs['ExclusiveStartKey'] = query_results['LastEvaluatedKey'] 42 | query_results = table.query(*args, **kwargs) 43 | for item in query_results['Items']: 44 | yield item 45 | 46 | table.get_existing_item = get_existing_item 47 | table.iter_query = iter_query 48 | 49 | 50 | add_extended_table_functions(Wheel) 51 | add_extended_table_functions(WheelParticipant) 52 | 53 | 54 | def check_string(string): 55 | return isinstance(string, str) and len(string) > 0 56 | 57 | 58 | def get_uuid(): 59 | return str(uuid.uuid4()) 60 | 61 | 62 | def get_utc_timestamp(): 63 | return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") 64 | 65 | 66 | def to_update_kwargs(attributes): 67 | """ 68 | For an attribute dictionary, make a default update expression for setting the values 69 | 70 | Notes: Use an expression attribute name to replace that attribute's name with reserved word in the expression, 71 | reference can be found here: 72 | http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#ExpressionAttributeNames 73 | """ 74 | 75 | return { 76 | 'UpdateExpression': 'set {}'.format(', '.join([f"#{k} = :{k}" for k in attributes])), 77 | 'ExpressionAttributeValues': {f":{k}": v for k, v in attributes.items()}, 78 | 'ExpressionAttributeNames': {f"#{k}": k for k in attributes} 79 | } 80 | -------------------------------------------------------------------------------- /api/wheel.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | from boto3.dynamodb.conditions import Key 15 | from utils import get_utc_timestamp, get_uuid, Wheel, WheelParticipant, check_string, to_update_kwargs 16 | import base 17 | import choice_algorithm 18 | 19 | 20 | @base.route('/wheel', methods=['PUT', 'POST']) 21 | def create_wheel(event): 22 | """ 23 | Create a wheel. Requires a name 24 | 25 | :param event: Lambda event containing the API Gateway request body including a name 26 | { 27 | "body": 28 | { 29 | "name": string wheel name, 30 | } 31 | } 32 | :return: response dictionary containing new wheel object if successful 33 | { 34 | "body": 35 | { 36 | "id": string ID of the wheel (DDB Hash Key), 37 | "name": string name of the wheel, 38 | "participant_count": number of participants in the wheel, 39 | "created_at": creation timestamp, 40 | "updated_at": updated timestamp, 41 | } 42 | } 43 | """ 44 | create_timestamp = get_utc_timestamp() 45 | body = event['body'] 46 | if body is None or not check_string(body.get('name', None)): 47 | raise base.BadRequestError( 48 | f"New wheels require a name that must be a string with a length of at least 1. Got: {body}" 49 | ) 50 | 51 | wheel = { 52 | 'id': get_uuid(), 53 | 'name': body['name'], 54 | 'created_at': create_timestamp, 55 | 'updated_at': create_timestamp, 56 | } 57 | with choice_algorithm.wrap_wheel_creation(wheel): 58 | Wheel.put_item(Item=wheel) 59 | return wheel 60 | 61 | 62 | @base.route('/wheel/{wheel_id}', methods=['DELETE']) 63 | def delete_wheel(event): 64 | """ 65 | Deletes the wheel and all of its participants 66 | 67 | :param event: Lambda event containing the API Gateway request path parameter wheel_id 68 | { 69 | "pathParameters": 70 | { 71 | "wheel_id": string ID of the wheel (DDB Hash Key) 72 | } 73 | } 74 | :return: response dictionary 75 | """ 76 | wheel_id = event['pathParameters']['wheel_id'] 77 | # DynamoDB always succeeds for delete_item, 78 | Wheel.delete_item(Key={'id': wheel_id}) 79 | 80 | # Clear out all participants of the wheel. Query will be empty if it was already deleted 81 | with WheelParticipant.batch_writer() as batch: 82 | query_params = { 83 | 'KeyConditionExpression': Key('wheel_id').eq(wheel_id), 84 | 'ProjectionExpression': 'id' 85 | } 86 | # We don't use the default generator here because we don't want the deletes to change the query results 87 | for p in list(WheelParticipant.iter_query(**query_params)): 88 | batch.delete_item(Key={'id': p['id'], 'wheel_id': wheel_id}) 89 | 90 | 91 | @base.route('/wheel/{wheel_id}', methods=['GET']) 92 | def get_wheel(event): 93 | """ 94 | Returns the wheel object corresponding to the given wheel_id 95 | 96 | :param event: Lambda event containing the API Gateway request path parameter wheel_id 97 | { 98 | "pathParameters": 99 | { 100 | "wheel_id": string ID of the wheel (DDB Hash Key) 101 | } 102 | } 103 | :return: response dictionary containing the requested wheel object if successful 104 | { 105 | "body": 106 | { 107 | "id": string ID of the wheel (DDB Hash Key), 108 | "name": string name of the wheel, 109 | "participant_count": number of participants in the wheel, 110 | "created_at": creation timestamp, 111 | "updated_at": updated timestamp, 112 | } 113 | } 114 | """ 115 | return Wheel.get_existing_item(Key={'id': event['pathParameters']['wheel_id']}) 116 | 117 | 118 | @base.route('/wheel', methods=['GET']) 119 | def list_wheels(event): 120 | """ 121 | Get all available wheels 122 | 123 | :param event: Lambda event containing query string parameters that are passed to Boto's scan() API for the wheel 124 | table 125 | { 126 | "queryStringParameters": 127 | { 128 | ... 129 | } 130 | } 131 | :return: List of wheels 132 | { 133 | "body": 134 | "Count": number of wheels, 135 | "Items": 136 | [ 137 | wheel1, 138 | wheel2, 139 | wheeln, 140 | ], 141 | "ScannedCount": number of items before queryStringParameters were applied, 142 | } 143 | } 144 | """ 145 | parameters = event.get('queryStringParameters', None) or {} 146 | return Wheel.scan(**parameters) 147 | 148 | 149 | @base.route('/wheel/{wheel_id}', methods=['PUT', 'POST']) 150 | def update_wheel(event): 151 | """ 152 | Update the name of the wheel and/or refresh its participant count 153 | 154 | :param event: Lambda event containing the API Gateway request path parameter wheel_id 155 | { 156 | "pathParameters": 157 | { 158 | "wheel_id": string ID of the wheel (DDB Hash Key) 159 | }, 160 | "body": 161 | { 162 | "id": string ID of the wheel (DDB Hash Key), 163 | "name": string name of the wheel, 164 | } 165 | } 166 | :return: response dictionary containing the updated wheel object if successful 167 | { 168 | "body": 169 | { 170 | "id": string ID of the wheel (DDB Hash Key), 171 | "name": string name of the wheel, 172 | "participant_count": number of participants in the wheel, 173 | "created_at": creation timestamp, 174 | "updated_at": updated timestamp, 175 | } 176 | } 177 | """ 178 | wheel_id = event['pathParameters']['wheel_id'] 179 | key = {'id': wheel_id} 180 | # Make sure wheel exists 181 | wheel = Wheel.get_existing_item(Key=key) 182 | name = event['body'].get('name', None) 183 | if not check_string(name): 184 | raise base.BadRequestError("Updating a wheel requires a new name of at least 1 character in length") 185 | 186 | update = {'name': name, 'updated_at': get_utc_timestamp()} 187 | Wheel.update_item(Key=key, **to_update_kwargs(update)) 188 | # Represent the change locally for successful responses 189 | wheel.update(update) 190 | return wheel 191 | 192 | 193 | @base.route('/wheel/{wheel_id}/reset', methods=['PUT', 'POST']) 194 | def reset_wheel(event): 195 | """ 196 | Resets the weights of all participants of the wheel 197 | 198 | :param event: Lambda event containing the API Gateway request path parameter wheel_id 199 | { 200 | "pathParameters": 201 | { 202 | "wheel_id": string ID of the wheel (DDB Hash Key) 203 | } 204 | } 205 | :return: response dictionary 206 | """ 207 | # Ensure that the wheel exists 208 | wheel_id = event['pathParameters']['wheel_id'] 209 | wheel = Wheel.get_existing_item(Key={'id': wheel_id}) 210 | choice_algorithm.reset_wheel(wheel) 211 | 212 | 213 | @base.route('/wheel/{wheel_id}/unrig', methods=['PUT', 'POST']) 214 | def unrig_participant(event): 215 | """ 216 | Remove rigging for the specified wheel 217 | 218 | :param event: Lambda event containing the API Gateway request path parameter wheel_id 219 | { 220 | "pathParameters": 221 | { 222 | "wheel_id": string ID of the wheel (DDB Hash Key) 223 | } 224 | } 225 | :return: response dictionary 226 | """ 227 | # By default, rigging the wheel isn't hidden but they can be 228 | wheel_id = event['pathParameters']['wheel_id'] 229 | 230 | Wheel.update_item(Key={'id': wheel_id}, UpdateExpression='remove rigging') 231 | -------------------------------------------------------------------------------- /api/wheel_participant.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | from boto3.dynamodb.conditions import Key 15 | from utils import get_utc_timestamp, get_uuid, Wheel, WheelParticipant, check_string, to_update_kwargs 16 | import base 17 | import choice_algorithm 18 | 19 | 20 | @base.route('/wheel/{wheel_id}/participant', methods=['PUT', 'POST']) 21 | def create_participant(event): 22 | """ 23 | Create a participant 24 | 25 | :param event: Lambda event containing the API Gateway request body including a name and a url and the 26 | path parameter wheel_id 27 | { 28 | "pathParameters": 29 | { 30 | "wheel_id": string ID of the wheel (DDB Hash Key) 31 | }, 32 | "body": 33 | { 34 | "name": participant name string, 35 | "url: Valid URL for the participant, 36 | } 37 | } 38 | :return: response dictionary containing new participant object if successful 39 | { 40 | "body": 41 | { 42 | "id": string ID of the participant (DDB Hash Key), 43 | "wheel_id": string ID of the wheel (DDB Hash Key), 44 | "name": string name of the wheel, 45 | "url: URL for the participant, 46 | "created_at": creation timestamp, 47 | "updated_at": updated timestamp, 48 | } 49 | } 50 | """ 51 | wheel_id = event['pathParameters']['wheel_id'] 52 | body = event['body'] 53 | if not check_string(body.get('name', None)) or not check_string(body.get('url', None)): 54 | raise base.BadRequestError("Participants require a name and url which must be at least 1 character in length") 55 | 56 | wheel = Wheel.get_existing_item(Key={'id': wheel_id}) 57 | create_timestamp = get_utc_timestamp() 58 | 59 | participant = { 60 | 'wheel_id': wheel_id, 61 | 'id': get_uuid(), 62 | 'name': body['name'], 63 | 'url': body['url'], 64 | 'created_at': create_timestamp, 65 | 'updated_at': create_timestamp, 66 | } 67 | with choice_algorithm.wrap_participant_creation(wheel, participant): 68 | WheelParticipant.put_item(Item=participant) 69 | return participant 70 | 71 | 72 | @base.route('/wheel/{wheel_id}/participant/{participant_id}', methods=['DELETE']) 73 | def delete_participant(event): 74 | """ 75 | Deletes the participant from the wheel and redistributes wheel weights 76 | 77 | :param event: Lambda event containing the API Gateway request path parameters wheel_id and participant_id 78 | { 79 | "pathParameters": 80 | { 81 | "wheel_id": string ID of the wheel (DDB Hash Key) 82 | "participant_id": string ID of the participant (DDB Hash Key) 83 | }, 84 | } 85 | :return: response dictionary 86 | """ 87 | wheel_id = event['pathParameters']['wheel_id'] 88 | participant_id = event['pathParameters']['participant_id'] 89 | # Make sure the wheel exists 90 | wheel = Wheel.get_existing_item(Key={'id': wheel_id}) 91 | 92 | # REST-ful Deletes are idempotent and should not error if it's already been deleted 93 | response = WheelParticipant.delete_item(Key={'wheel_id': wheel_id, 'id': participant_id}, ReturnValues='ALL_OLD') 94 | if 'Attributes' in response: 95 | choice_algorithm.on_participant_deletion(wheel, response['Attributes']) 96 | 97 | 98 | @base.route('/wheel/{wheel_id}/participant', methods=['GET']) 99 | def list_participants(event): 100 | """ 101 | Gets the participants for the specified wheel_id 102 | 103 | :param event: Lambda event containing the API Gateway request path parameter wheel_id 104 | { 105 | "pathParameters": 106 | { 107 | "wheel_id": string ID of the wheel (DDB Hash Key) 108 | }, 109 | } 110 | :return: response dictionary containing a list of participants 111 | { 112 | "body": 113 | [ 114 | participant1, 115 | participant2, 116 | ... 117 | participantn, 118 | ] 119 | } 120 | """ 121 | wheel_id = event['pathParameters']['wheel_id'] 122 | # Make sure the wheel exists 123 | Wheel.get_existing_item(Key={'id': wheel_id}) 124 | return list(WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel_id))) 125 | 126 | 127 | @base.route('/wheel/{wheel_id}/participant/{participant_id}', methods=['PUT', 'POST']) 128 | def update_participant(event): 129 | """ 130 | Update a participant's name and/or url 131 | 132 | :param event: Lambda event containing the API Gateway request body including updated name or url and the 133 | path parameters wheel_id and participant_id 134 | { 135 | "pathParameters": 136 | { 137 | "wheel_id": string ID of the wheel (DDB Hash Key) 138 | "participant_id": string ID of the participant (DDB Hash Key) 139 | }, 140 | "body": 141 | { 142 | "id": string ID of the participant (DDB Hash Key), 143 | "name": string name of the wheel (optional), 144 | "url: Valid URL for the participant (optional), 145 | } 146 | } 147 | :return: response dictionary containing the updated participant object if successful 148 | { 149 | "body": 150 | { 151 | "id": string ID of the participant (DDB Hash Key), 152 | "wheel_id": string ID of the wheel (DDB Hash Key), 153 | "name": string name of the wheel, 154 | "url: URL for the participant, 155 | "created_at": creation timestamp, 156 | "updated_at": updated timestamp, 157 | } 158 | } 159 | """ 160 | wheel_id = event['pathParameters']['wheel_id'] 161 | participant_id = event['pathParameters']['participant_id'] 162 | # Check that the participant exists 163 | participant = WheelParticipant.get_existing_item(Key={'id': participant_id, 'wheel_id': wheel_id}) 164 | body = event['body'] 165 | params = {'updated_at': get_utc_timestamp()} 166 | if not check_string(body.get('name', 'Not Specified')) or not check_string(body.get('url', 'Not Specified')): 167 | raise base.BadRequestError("Participants names and urls must be at least 1 character in length") 168 | 169 | if 'name' in body: 170 | params['name'] = body['name'] 171 | 172 | if 'url' in body: 173 | params['url'] = body['url'] 174 | 175 | WheelParticipant.update_item(Key={'id': participant_id, 'wheel_id': wheel_id}, **to_update_kwargs(params)) 176 | participant.update(params) 177 | return participant 178 | 179 | 180 | @base.route('/wheel/{wheel_id}/participant/{participant_id}/select', methods=['PUT', 'POST']) 181 | def select_participant(event): 182 | """ 183 | Indicates selection of a participant by the wheel. This will cause updates to the weights for all participants 184 | or removal of rigging if the wheel is rigged. 185 | 186 | :param event: Lambda event containing the API Gateway request path parameters wheel_id and participant_id 187 | { 188 | "pathParameters": 189 | { 190 | "wheel_id": string ID of the wheel to rig (DDB Hash Key) 191 | "participant_id": string ID of the participant to rig (DDB Hash Key) 192 | }, 193 | } 194 | :return: response dictionary 195 | """ 196 | wheel_id = event['pathParameters']['wheel_id'] 197 | participant_id = event['pathParameters']['participant_id'] 198 | wheel = Wheel.get_existing_item(Key={'id': wheel_id}) 199 | participant = WheelParticipant.get_existing_item(Key={'id': participant_id, 'wheel_id': wheel_id}) 200 | choice_algorithm.select_participant(wheel, participant) 201 | 202 | # Undo any rigging that has been set up 203 | Wheel.update_item(Key={'id': wheel['id']}, UpdateExpression='remove rigging') 204 | 205 | 206 | @base.route('/wheel/{wheel_id}/participant/{participant_id}/rig', methods=['PUT', 'POST']) 207 | def rig_participant(event): 208 | """ 209 | Rig the specified wheel for the specified participant. Default behavior is comical rigging (hidden == False) 210 | but hidden can be specified to indicate deceptive rigging (hidden == True) 211 | 212 | :param event: Lambda event containing the API Gateway request path parameters wheel_id and participant_id 213 | { 214 | "pathParameters": 215 | { 216 | "wheel_id": string ID of the wheel to rig (DDB Hash Key) 217 | "participant_id": string ID of the participant to rig (DDB Hash Key) 218 | }, 219 | "body": 220 | { 221 | "hidden": boolean indicates deceptive rigging if True, comical if False 222 | } 223 | } 224 | :return: response dictionary 225 | """ 226 | # By default, rigging the wheel isn't hidden but they can be 227 | wheel_id = event['pathParameters']['wheel_id'] 228 | participant_id = event['pathParameters']['participant_id'] 229 | hidden = bool(event['body'].get('hidden', False)) 230 | update = {'rigging': {'participant_id': participant_id, 'hidden': hidden}} 231 | Wheel.update_item(Key={'id': wheel_id}, **to_update_kwargs(update)) 232 | 233 | 234 | @base.route('/wheel/{wheel_id}/participant/suggest', methods=['GET']) 235 | def suggest_participant(event): 236 | """ 237 | Returns a suggested participant to be selected by the next wheel spin 238 | 239 | :param event: Lambda event containing the API Gateway request path parameter wheel_id 240 | { 241 | "pathParameters": 242 | { 243 | "wheel_id": string ID of the wheel (DDB Hash Key) 244 | }, 245 | } 246 | :return: response dictionary containing a selected participant_id 247 | { 248 | "body": 249 | { 250 | "participant_id": string ID of the suggested participant (DDB Hash Key), 251 | "rigged": True (if rigged, otherwise this key is not present) 252 | } 253 | } 254 | """ 255 | wheel_id = event['pathParameters']['wheel_id'] 256 | wheel = Wheel.get_existing_item(Key={'id': wheel_id}) 257 | if 'rigging' in wheel: 258 | participant_id = wheel['rigging']['participant_id'] 259 | # Use rigging only if the rigged participant is still available 260 | if 'Item' in WheelParticipant.get_item(Key={'wheel_id': wheel_id, 'id': participant_id}): 261 | return_value = {'participant_id': participant_id} 262 | # Only return rigged: True if we're not using hidden rigging 263 | if not wheel['rigging'].get('hidden', False): 264 | return_value['rigged'] = True 265 | return return_value 266 | return {'participant_id': choice_algorithm.suggest_participant(wheel)} 267 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | build: 5 | commands: 6 | # Install node 7 | - curl -sL https://deb.nodesource.com/setup_14.x | bash - 8 | - apt-get install -y nodejs 9 | # Run UI tests 10 | - cd ui/ 11 | - npm install 12 | - npm run test 13 | - cd ../ 14 | # Run API tests 15 | - pip install --upgrade pip 16 | - pip install --upgrade -r requirements.txt 17 | - cd api/ 18 | - AWS_ACCESS_KEY_ID=None AWS_SECRET_ACCESS_KEY=None AWS_DEFAULT_REGION=us-west-2 pytest --verbose --cov-report term-missing --cov ./ -s 19 | - cd ../ 20 | # Fix permissions for broken ZIP process from GitHub 21 | - chmod a+x run 22 | - chmod a+x _compile_cloudformation_template.py 23 | # Compile the application 24 | - ./run build 25 | # Upload static website assets 26 | - mkdir deploy 27 | - export BUILD_ID=`echo $CODEBUILD_BUILD_ARN | awk -F':' '{print $NF}'` 28 | - aws s3 mv --recursive build/static_* s3://$WEBSITE_ASSETS_BUCKET/$BUILD_ID 29 | # Compile CloudFormation templates 30 | - cp -vRL build deploy/build 31 | - cp _compile_cloudformation_template.py build 32 | - ./build/_compile_cloudformation_template.py ./cloudformation deploy "https://s3-$AWS_REGION.amazonaws.com/$WEBSITE_ASSETS_BUCKET/$BUILD_ID" 33 | # Package CloudFormation templates 34 | - mkdir deploy/compiled_templates 35 | - aws cloudformation package --template-file deploy/cognito.yml --s3-bucket $ARTIFACTS_BUCKET --output-template-file deploy/compiled_templates/cognito.yml 36 | - aws cloudformation package --template-file deploy/lambda.yml --s3-bucket $ARTIFACTS_BUCKET --output-template-file deploy/compiled_templates/lambda.yml 37 | - aws cloudformation package --template-file deploy/api_gateway.yml --s3-bucket $ARTIFACTS_BUCKET --output-template-file deploy/compiled_templates/api_gateway.yml 38 | - aws cloudformation package --template-file deploy/api_gateway_lambda_roles.yml --s3-bucket $ARTIFACTS_BUCKET --output-template-file deploy/compiled_templates/api_gateway_lambda_roles.yml 39 | - aws cloudformation package --template-file deploy/aws-ops-wheel.yml --s3-bucket $ARTIFACTS_BUCKET --output-template-file deploy/compiled_templates/aws-ops-wheel.yml 40 | 41 | artifacts: 42 | files: 43 | - 'deploy/compiled_templates/aws-ops-wheel.yml' 44 | discard-paths: yes 45 | -------------------------------------------------------------------------------- /cloudformation/api_gateway.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | AWSTemplateFormatVersion: '2010-09-09' 15 | Description: 'AWS Ops Wheel - API Gateway Stack' 16 | Parameters: 17 | AdminEmail: 18 | Type: String 19 | Resources: 20 | AWSOpsWheelAPI: 21 | Properties: {Name: AWSOpsWheel} 22 | Type: AWS::ApiGateway::RestApi 23 | AWSOpsWheelAPIApp: 24 | Type: AWS::ApiGateway::Deployment 25 | Properties: 26 | Description: Deployment of the AWS Ops Wheel API 27 | RestApiId: {Ref: AWSOpsWheelAPI} 28 | StageName: app 29 | AWSOpsWheelS3Role: 30 | Properties: 31 | AssumeRolePolicyDocument: 32 | Statement: 33 | - Action: ['sts:AssumeRole'] 34 | Effect: Allow 35 | Principal: 36 | Service: [apigateway.amazonaws.com] 37 | Version: '2012-10-17' 38 | ManagedPolicyArns: ['arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess'] 39 | Path: /service-role/ 40 | Type: AWS::IAM::Role 41 | Outputs: 42 | Endpoint: 43 | Value: 44 | Fn::Sub: 'https://${AWSOpsWheelAPI}.execute-api.${AWS::Region}.amazonaws.com/app/' 45 | -------------------------------------------------------------------------------- /cloudformation/api_gateway_lambda_roles.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | AWSTemplateFormatVersion: '2010-09-09' 15 | Description: 'AWS Ops Wheel - API Gateway permissions for Lambda' -------------------------------------------------------------------------------- /cloudformation/aws-ops-wheel.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | AWSTemplateFormatVersion: '2010-09-09' 15 | Description: 'AWS Ops Wheel - Main Stack Orchestrator' -------------------------------------------------------------------------------- /cloudformation/awsopswheel-create-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "codepipeline:CreatePipeline", 8 | "s3:CreateBucket", 9 | "iam:CreateRole", 10 | "s3:ListBucket", 11 | "iam:AttachRolePolicy", 12 | "iam:PutRolePolicy", 13 | "dynamodb:DeleteTable", 14 | "codepipeline:DeletePipeline", 15 | "s3:GetBucketPolicy", 16 | "iam:PassRole", 17 | "iam:DetachRolePolicy", 18 | "dynamodb:DescribeTable", 19 | "iam:DeleteRolePolicy", 20 | "codepipeline:GetPipeline", 21 | "s3:PutBucketAcl", 22 | "cloudformation:UpdateStack", 23 | "events:RemoveTargets", 24 | "lambda:DeleteFunction", 25 | "iam:ListRolePolicies", 26 | "s3:DeleteBucket", 27 | "s3:PutBucketVersioning", 28 | "cloudformation:ListStackResources", 29 | "iam:GetRole", 30 | "events:DescribeRule", 31 | "apigateway:*", 32 | "iam:UpdateRoleDescription", 33 | "iam:DeleteRole", 34 | "s3:DeleteBucketPolicy", 35 | "codebuild:CreateProject", 36 | "cloudformation:DescribeStacks", 37 | "dynamodb:CreateTable", 38 | "events:PutTargets", 39 | "events:DeleteRule", 40 | "lambda:UpdateFunctionCode", 41 | "codecommit:*", 42 | "lambda:AddPermission", 43 | "s3:PutBucketLogging", 44 | "cloudformation:CreateStack", 45 | "cloudformation:DeleteStack", 46 | "s3:PutBucketPolicy", 47 | "codebuild:DeleteProject", 48 | "codepipeline:GetPipelineState", 49 | "s3:GetBucketLocation", 50 | "iam:GetRolePolicy", 51 | "lambda:RemovePermission", 52 | "dynamodb:UpdateTable", 53 | "lambda:GetFunction", 54 | "s3:GetEncryptionConfiguration", 55 | "s3:PutEncryptionConfiguration" 56 | ], 57 | "Resource": [ 58 | "arn:aws:s3:::awsopswheelsourcebucket-*", 59 | "arn:aws:s3:::awsopswheel-*", 60 | "arn:aws:dynamodb:*:*:table/AWSOpsWheel-*", 61 | "arn:aws:iam::*:role/AWSOpsWheel-*", 62 | "arn:aws:iam::*:role/service-role/AWSOpsWheel-*", 63 | "arn:aws:codecommit:*:*:AWSOpsWheel*", 64 | "arn:aws:codebuild:*:*:project/AWSOpsWheel*", 65 | "arn:aws:events:*:*:rule/AWSOpsWheel-*", 66 | "arn:aws:cloudformation:*:*:stack/AWSOpsWheelSourceBucket/*", 67 | "arn:aws:cloudformation:*:*:stack/AWSOpsWheel/*", 68 | "arn:aws:cloudformation:*:*:stack/AWSOpsWheel-*/*", 69 | "arn:aws:apigateway:*::/restapis", 70 | "arn:aws:apigateway:*::/restapis/*", 71 | "arn:aws:codepipeline:*:*:AWSOpsWheel*", 72 | "arn:aws:lambda:*:*:function:AWSOpsWheel-*" 73 | ] 74 | }, 75 | { 76 | "Effect": "Allow", 77 | "Action": [ 78 | "lambda:CreateFunction", 79 | "cloudformation:ListStacks", 80 | "cognito-identity:*", 81 | "dynamodb:UntagResource", 82 | "dynamodb:ListTables", 83 | "events:PutRule", 84 | "lambda:UpdateFunctionConfiguration", 85 | "iam:ListRoles", 86 | "codecommit:CreateRepository", 87 | "codecommit:ListRepositories", 88 | "cognito-sync:*", 89 | "dynamodb:TagResource", 90 | "iam:ListOpenIDConnectProviders", 91 | "cognito-idp:*", 92 | "codebuild:ListProjects", 93 | "sns:ListPlatformApplications" 94 | ], 95 | "Resource": "*" 96 | }, 97 | { 98 | "Effect": "Allow", 99 | "Action": [ 100 | "codebuild:BatchGetProjects", 101 | "s3:PutObject", 102 | "s3:GetObject", 103 | "s3:DeleteObjectVersion", 104 | "s3:DeleteObject", 105 | "s3:PutObjectAcl" 106 | ], 107 | "Resource": [ 108 | "arn:aws:codebuild:*:*:project/AWSOpsWheel*", 109 | "arn:aws:s3:::awsopswheelsourcebucket-*/*", 110 | "arn:aws:s3:::awsopswheel-*/*" 111 | ] 112 | }, 113 | { 114 | "Effect": "Allow", 115 | "Action": "iam:ListRoles", 116 | "Resource": [ 117 | "arn:aws:iam::*:role/AWSOpsWheel-*", 118 | "arn:aws:iam::*:role/service-role/AWSOpsWheel-*" 119 | ] 120 | } 121 | ] 122 | } -------------------------------------------------------------------------------- /cloudformation/cognito.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | AWSTemplateFormatVersion: '2010-09-09' 15 | Description: 'AWS Ops Wheel - Cognito Stack' 16 | Parameters: 17 | AdminEmail: 18 | Type: String 19 | Outputs: 20 | CognitoUserPoolArn: 21 | Value: 22 | Fn::GetAtt: CognitoUserPool.Arn 23 | Resources: 24 | CognitoUserPool: 25 | Type: AWS::Cognito::UserPool 26 | Properties: 27 | UserPoolName: 28 | Ref: AWS::StackName 29 | AdminCreateUserConfig: 30 | AllowAdminCreateUserOnly: true 31 | UnusedAccountValidityDays: 90 32 | InviteMessageTemplate: 33 | EmailMessage: 'Your AWS Ops Wheel username is {username} and the temporary password is {####}' 34 | EmailSubject: 'Your temporary password for AWS Ops Wheel ' 35 | SMSMessage: 'Your AWS Ops Wheel username is {username} and the temporary password is {####}' 36 | AutoVerifiedAttributes: 37 | - email 38 | Policies: 39 | PasswordPolicy: 40 | MinimumLength: 6 41 | RequireLowercase: false 42 | RequireNumbers: false 43 | RequireSymbols: false 44 | RequireUppercase: false 45 | CognitoUserPoolClient: 46 | Type: AWS::Cognito::UserPoolClient 47 | Properties: 48 | ClientName: WheelUIClient 49 | UserPoolId: 50 | Ref: CognitoUserPool 51 | CognitoUserPoolAdmin: 52 | Type: AWS::Cognito::UserPoolUser 53 | Properties: 54 | Username: admin 55 | DesiredDeliveryMediums: 56 | - EMAIL 57 | UserPoolId: 58 | Ref: CognitoUserPool 59 | UserAttributes: 60 | - Name: email 61 | Value: 62 | Ref: AdminEmail 63 | -------------------------------------------------------------------------------- /cloudformation/lambda.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | AWSTemplateFormatVersion: 2010-09-09 15 | Description: 'AWS Ops Wheel - Lambda Stack' 16 | Resources: 17 | AWSOpsWheelLambdaRole: 18 | Properties: 19 | AssumeRolePolicyDocument: 20 | Statement: 21 | - Action: ['sts:AssumeRole'] 22 | Effect: Allow 23 | Principal: 24 | Service: [lambda.amazonaws.com] 25 | Version: '2012-10-17' 26 | ManagedPolicyArns: ['arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'] 27 | Path: /service-role/ 28 | Type: AWS::IAM::Role 29 | AWSOpsWheelLambdaPolicy: 30 | Properties: 31 | Groups: [] 32 | PolicyDocument: 33 | Statement: 34 | - Action: ['dynamodb:DeleteItem', 'dynamodb:GetItem', 'dynamodb:PutItem', 35 | 'dynamodb:Scan', 'dynamodb:Query', 'dynamodb:UpdateItem', 'dynamodb:BatchWriteItem'] 36 | Effect: Allow 37 | Resource: 38 | Fn::Join: 39 | - '' 40 | - - 'arn:aws:dynamodb:' 41 | - { Ref: 'AWS::Region' } 42 | - ':' 43 | - { Ref: 'AWS::AccountId' } 44 | - ':' 45 | - table/* 46 | Version: '2012-10-17' 47 | PolicyName: AWSOpsWheelLambdaPolicy 48 | Roles: 49 | - {Ref: AWSOpsWheelLambdaRole} 50 | Users: [] 51 | Type: AWS::IAM::Policy 52 | participantDynamoDBTable: 53 | Properties: 54 | AttributeDefinitions: 55 | - { AttributeName: id, AttributeType: S } 56 | - { AttributeName: wheel_id, AttributeType: S } 57 | KeySchema: 58 | - { AttributeName: wheel_id, KeyType: HASH } 59 | - { AttributeName: id, KeyType: RANGE } 60 | BillingMode: PAY_PER_REQUEST 61 | Type: AWS::DynamoDB::Table 62 | wheelDynamoDBTable: 63 | Properties: 64 | AttributeDefinitions: 65 | - { AttributeName: id, AttributeType: S } 66 | - { AttributeName: name, AttributeType: S } 67 | KeySchema: 68 | - { AttributeName: id, KeyType: HASH } 69 | BillingMode: PAY_PER_REQUEST 70 | GlobalSecondaryIndexes: 71 | - IndexName: name_index 72 | KeySchema: 73 | - { AttributeName: name, KeyType: HASH } 74 | Projection: 75 | ProjectionType: ALL 76 | Type: AWS::DynamoDB::Table 77 | -------------------------------------------------------------------------------- /cloudformation/source-bucket.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). 4 | # You may not use this file except in compliance with the License. 5 | # A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | AWSTemplateFormatVersion: 2010-09-09 15 | Description: 'AWS Ops Wheel - Source Code Bucket' 16 | Resources: 17 | SourceS3Bucket: 18 | Type: AWS::S3::Bucket 19 | Properties: 20 | VersioningConfiguration: 21 | Status: Enabled 22 | AccessControl: BucketOwnerRead 23 | BucketEncryption: 24 | ServerSideEncryptionConfiguration: 25 | - ServerSideEncryptionByDefault: 26 | SSEAlgorithm: AES256 27 | -------------------------------------------------------------------------------- /hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). 6 | # You may not use this file except in compliance with the License. 7 | # A copy of the License is located at 8 | # 9 | # http://aws.amazon.com/apache2.0/ 10 | # 11 | # or in the "license" file accompanying this file. This file is distributed 12 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 13 | # express or implied. See the License for the specific language governing 14 | # permissions and limitations under the License. 15 | 16 | while read local_ref local_sha remote_ref remote_sha 17 | do 18 | branch=$(git rev-parse --symbolic --abbrev-ref $remote_ref) 19 | if [ "release" == "$branch" ]; then 20 | # Ensure public github is set to external remote 21 | if [ $(git remote | grep external | wc -l) == 0 ]; then 22 | git remote add external https://github.com/aws/aws-ops-wheel.git 23 | fi 24 | # Check for release being in sync with most recent version of github repo 25 | if [ $(git diff origin/release external/master -- . ':!hooks' | wc -l) -gt 0 ]; then 26 | echo "release does not have the latest changes to the external repository." 27 | echo "Please pull those changes in before trying to push anything else to this branch." 28 | exit 1 29 | fi 30 | # Ensure a single commit is being pushed at a time 31 | if [ $(git log --pretty=oneline @{u}.. | wc -l) -gt 1 ]; then 32 | echo "You are trying to push multiple commits:" 33 | git log --pretty=oneline @{u}.. 34 | echo "Please rebase and squash commits to continue." 35 | exit 1 36 | fi 37 | fi 38 | done 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | awscli 2 | boto3 3 | pyaml 4 | pytest 5 | pytest-cov 6 | moto 7 | -------------------------------------------------------------------------------- /screenshots/participants_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-ops-wheel/34319e4ddffee51635a837feec0b3b4076acd16b/screenshots/participants_table.png -------------------------------------------------------------------------------- /screenshots/wheel_post_spin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-ops-wheel/34319e4ddffee51635a837feec0b3b4076acd16b/screenshots/wheel_post_spin.png -------------------------------------------------------------------------------- /screenshots/wheel_pre_spin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-ops-wheel/34319e4ddffee51635a837feec0b3b4076acd16b/screenshots/wheel_pre_spin.png -------------------------------------------------------------------------------- /screenshots/wheels_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-ops-wheel/34319e4ddffee51635a837feec0b3b4076acd16b/screenshots/wheels_table.png -------------------------------------------------------------------------------- /ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /ui/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/editions/.* 3 | .*/node_modules/.*/node_modules/.* 4 | .*/build/.* 5 | .*/.git/.* 6 | .*/.idea/.* 7 | .*/.nyc_output/.* 8 | 9 | [include] 10 | .*/src/.* 11 | 12 | [libs] 13 | flow-typed 14 | 15 | [lints] 16 | all=warn 17 | 18 | [options] 19 | include_warnings=true 20 | -------------------------------------------------------------------------------- /ui/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "full-trace": true, 3 | "exit": true, 4 | "extension": ["js", "jsx"], 5 | "recursive": true, 6 | "require": [ 7 | "./test/setup.js", 8 | "core-js/stage/4", 9 | "@babel/register", 10 | "./test/globals.jsx", 11 | "source-map-support/register", 12 | "ignore-styles" 13 | ], 14 | "watch-files": ["test/*.test.js*", "test/**/*.test.js*"] 15 | } -------------------------------------------------------------------------------- /ui/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/setup.js 2 | --require core-js/stage/4 3 | --require @babel/register 4 | --require ./test/globals.jsx 5 | --require source-map-support/register 6 | --require ignore-styles 7 | --full-trace 8 | --watch-extensions js,jsx 9 | --recursive 10 | --exit 11 | test/*.test.js* 12 | test/**/*.test.js* 13 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "One-page Web Application for the AWS Ops Wheel. README at https://github.com/aws/aws-ops-wheel", 3 | "license": "Apache-2.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/aws/aws-ops-wheel.git" 7 | }, 8 | "scripts": { 9 | "test": "npm run coverage", 10 | "coverage": "nyc mocha --config .mocharc.json", 11 | "build": "npm run build-dev && npm run build-prod", 12 | "build-dev": "NODE_ENV=development webpack --progress --color --mode development", 13 | "build-prod": "NODE_ENV=production webpack --progress --color --mode production", 14 | "unit-test": "mocha --config .mocharc.json", 15 | "test-watch": "mocha --config .mocharc.json --watch", 16 | "start": "webpack-dev-server --compress --host 0.0.0.0", 17 | "flow": "flow", 18 | "preinstall": "npx npm-force-resolutions" 19 | }, 20 | "nyc": { 21 | "include": [ 22 | "src/*.jsx", 23 | "src/**/*.jsx" 24 | ], 25 | "extension": [ 26 | ".js", 27 | ".jsx" 28 | ], 29 | "require": [ 30 | "core-js/stage/4", 31 | "@babel/register", 32 | "source-map-support/register", 33 | "ignore-styles" 34 | ], 35 | "reporter": [ 36 | "text", 37 | "text-summary" 38 | ], 39 | "sourceMap": false, 40 | "instrument": false, 41 | "cache": false, 42 | "all": true 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.6.4", 46 | "@babel/plugin-proposal-class-properties": "^7.5.5", 47 | "@babel/preset-env": "^7.6.3", 48 | "@babel/preset-flow": "^7.0.0", 49 | "@babel/preset-react": "^7.6.3", 50 | "@babel/register": "^7.6.2", 51 | "babel-loader": "^8.0.6", 52 | "babel-plugin-istanbul": "^5.1.4", 53 | "canvas": "^2.9.0", 54 | "chai": "^4.1.2", 55 | "chai-enzyme": "^1.0.0-beta.1", 56 | "cheerio": "^1.0.0-rc.10", 57 | "css-loader": "^6.8.1", 58 | "enzyme": "^3.10.0", 59 | "enzyme-adapter-react-16": "^1.15.1", 60 | "file-loader": "^6.1.1", 61 | "flow-bin": "0.59.0", 62 | "flow-typed": "^3.8.0", 63 | "glob-parent": "^5.1.2", 64 | "history": "^4.7.2", 65 | "html-loader": "^5.0.0", 66 | "html-webpack-plugin": "^5.3.2", 67 | "ignore-styles": "^5.0.1", 68 | "jsdom": "^16.7.0", 69 | "jsdom-global": "^3.0.2", 70 | "mini-css-extract-plugin": "^2.6.1", 71 | "minimist": "^1.2.6", 72 | "mocha": "^9.2.2", 73 | "nock": "^9.1.0", 74 | "nyc": "^15.1.0", 75 | "sinon": "^7.3.2", 76 | "source-map-loader": "^0.2.3", 77 | "ssri": ">=8.0.1", 78 | "style-loader": "^3.3.1", 79 | "webpack": "^5.95.0", 80 | "webpack-dev-server": "^4.3.1", 81 | "y18n": ">=3.2.2" 82 | }, 83 | "dependencies": { 84 | "amazon-cognito-identity-js": "^2.0.30", 85 | "bootstrap": "^3.4.1", 86 | "core-js": "^2.5.1", 87 | "deep-extend": "^0.5.1", 88 | "es6-promise": "^4.1.1", 89 | "extend": "^3.0.2", 90 | "font-awesome": "^4.7.0", 91 | "isomorphic-fetch": "^3.0.0", 92 | "just-extend": "^4.0.0", 93 | "lodash": "^4.17.21", 94 | "moment": "^2.29.4", 95 | "moment-timezone": "^0.5.35", 96 | "node-fetch": ">=3.2.10", 97 | "pixi.js": "^4.6.2", 98 | "randomatic": "^3.0.0", 99 | "react": "^16.13.1", 100 | "react-bootstrap": "^0.31.5", 101 | "react-dom": "^16.13.1", 102 | "react-redux": "^5.0.6", 103 | "react-redux-fetch": "^0.15.0", 104 | "react-router": "^4.2.0", 105 | "react-router-bootstrap": "^0.24.4", 106 | "react-router-dom": "^4.2.2", 107 | "redux": "^4.0.0", 108 | "redux-logger": "^3.0.6", 109 | "redux-thunk": "^2.2.0", 110 | "sshpk": "^1.13.2", 111 | "url": "^0.11.0", 112 | "url-parse": "^1.5.8", 113 | "validate.js": "^0.12.0", 114 | "webpack-cli": "^4.8.0", 115 | "yargs-parser": "^20.2.7" 116 | }, 117 | "name": "ui", 118 | "version": "1.0.0" 119 | } 120 | -------------------------------------------------------------------------------- /ui/src/components/app.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {PropTypes, Component} from 'react'; 17 | import WheelTable from './wheel_table/wheel_table'; 18 | import Wheel from './wheel'; 19 | import ParticipantTable from './participant_table/participant_table'; 20 | import Login from './login'; 21 | import NotFound from './notFound'; 22 | import Navigation from './navigation'; 23 | import connect, {container} from 'react-redux-fetch'; 24 | import {Route, Switch} from 'react-router-dom'; 25 | import '../styles.css'; 26 | import {CognitoUserPool} from 'amazon-cognito-identity-js'; 27 | import {BrowserRouter, Router} from 'react-router-dom'; 28 | import {apiURL} from '../util'; 29 | 30 | export interface AppProps { 31 | dispatchConfigGet: PropTypes.func, 32 | configFetch: PropTypes.object, 33 | } 34 | export interface AppState { 35 | cognitoUserPool: Object | undefined, 36 | cognitoSession: Object | undefined, 37 | } 38 | 39 | /** 40 | * Main Application component 41 | */ 42 | class App extends Component { 43 | constructor(props) { 44 | super(props); 45 | this.state = { 46 | cognitoUserPool: undefined, 47 | cognitoSession: undefined 48 | }; 49 | } 50 | 51 | componentDidMount() { 52 | this.props.dispatchConfigGet(); 53 | } 54 | 55 | componentDidUpdate() { 56 | if (this.state.cognitoUserPool === undefined && this.props.configFetch.fulfilled) { 57 | const cognitoUserPool = new CognitoUserPool({ 58 | UserPoolId: this.props.configFetch.value.USER_POOL_ID, 59 | ClientId: this.props.configFetch.value.APP_CLIENT_ID, 60 | }) 61 | this.setState({cognitoUserPool}, this.refreshSession); 62 | } 63 | } 64 | 65 | refreshSession = () => { 66 | const currentUser = this.state.cognitoUserPool.getCurrentUser() 67 | if (currentUser !== null) { 68 | const app = this; // Necessary because of 'this' getting overridden in the callback 69 | currentUser.getSession(function(err, session) { 70 | if (err) { 71 | console.error(err); 72 | return; 73 | } 74 | container.registerRequestHeader('Authorization', session.getIdToken().getJwtToken()); 75 | app.setState({cognitoSession: session}); 76 | }) 77 | } 78 | } 79 | 80 | userLogout = () => { 81 | this.state.cognitoUserPool.getCurrentUser().signOut(); 82 | this.setState({cognitoUserPool: undefined, cognitoSession: undefined}); 83 | } 84 | 85 | handleClickCapture = () => { 86 | if(this.state.cognitoUserPool!== undefined){ 87 | this.refreshSession() 88 | }else{ 89 | this.componentDidUpdate() 90 | } 91 | } 92 | 93 | render() { 94 | if (!this.props.configFetch.fulfilled) { 95 | return (
Loading ...
); 96 | } 97 | 98 | const childProps = { 99 | userHasAuthenticated: this.refreshSession, 100 | userPool: new CognitoUserPool({ 101 | UserPoolId: this.props.configFetch.value.USER_POOL_ID, 102 | ClientId: this.props.configFetch.value.APP_CLIENT_ID, 103 | }), 104 | userLogout: this.userLogout 105 | }; 106 | 107 | if (this.state.cognitoSession !== undefined && this.state.cognitoSession.isValid()) { 108 | return ( 109 | 110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
119 |
120 | ) 121 | } else { 122 | return ; 123 | } 124 | } 125 | } 126 | 127 | export default connect([ 128 | { 129 | resource: 'config', 130 | method: 'get', 131 | request: {url: apiURL('config')}, 132 | }, 133 | ]) (App); 134 | -------------------------------------------------------------------------------- /ui/src/components/confirmation_modal.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {Modal, Button} from 'react-bootstrap'; 18 | 19 | interface ConfirmationModalProps { 20 | message: string; 21 | isModalOpen: boolean; 22 | onConfirm: Function; 23 | closeModal: Function; 24 | } 25 | 26 | export default class ConfirmationModal extends React.Component { 27 | constructor(props: ConfirmationModalProps) { 28 | super(props); 29 | this.close = this.close.bind(this); 30 | this.onConfirm = this.onConfirm.bind(this); 31 | } 32 | 33 | close() { 34 | this.props.closeModal(); 35 | } 36 | 37 | onConfirm() { 38 | this.props.onConfirm(); 39 | this.close(); 40 | } 41 | 42 | render() { 43 | const {message, isModalOpen} = this.props; 44 | return ( 45 | 49 | 50 | Are you sure? 51 | 52 | {message} 53 | 54 | 61 | 68 | 69 | 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ui/src/components/login.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, { Component, PropTypes } from "react"; 17 | import {Alert, Button, FormGroup, FormControl, ControlLabel} from "react-bootstrap"; 18 | import {AuthenticationDetails, CognitoUser, CognitoUserPool} from "amazon-cognito-identity-js"; 19 | 20 | interface LoginProps { 21 | userHasAuthenticated: PropTypes.func, 22 | userPool: CognitoUserPool | undefined, 23 | } 24 | 25 | interface LoginState { 26 | username: string, 27 | password: string, 28 | passwordChangeAttributes: Object, 29 | error: Object, 30 | isInFlight: boolean, 31 | user: CognitoUser | undefined, 32 | } 33 | 34 | export default class Login extends Component { 35 | constructor(props) { 36 | super(props); 37 | 38 | this.state = { 39 | username: '', 40 | password: '', 41 | passwordChangeAttributes: undefined, 42 | error: undefined, 43 | isInFlight: false, 44 | user: undefined, 45 | }; 46 | } 47 | 48 | // amazon-cognito-identity-js CognitoUser.authenticateUser() Callback 49 | onSuccess = () => { 50 | this.setState({passwordChangeAttributes: undefined, error: undefined, isInFlight: false}); 51 | this.props.userHasAuthenticated(true); 52 | }; 53 | 54 | // amazon-cognito-identity-js CognitoUser.authenticateUser() Callback 55 | onFailure = (error) => { 56 | this.setState({error, isInFlight: false}); 57 | }; 58 | 59 | // amazon-cognito-identity-js CognitoUser.authenticateUser() Callback 60 | newPasswordRequired = (userAttributes: Object) => { 61 | // User was signed up by an admin and must provide new 62 | // password and required attributes, if any, to complete 63 | // authentication. 64 | 65 | // the api doesn't accept this field back 66 | delete userAttributes.email_verified; 67 | delete userAttributes.email; 68 | this.setState({passwordChangeAttributes: userAttributes, error: undefined, password: '', isInFlight: false}); 69 | }; 70 | 71 | submitNewPassword = (event) => { 72 | event.preventDefault(); 73 | this.setState({isInFlight: true}); 74 | this.state.user.completeNewPasswordChallenge(this.state.password, this.state.passwordChangeAttributes, this); 75 | }; 76 | 77 | handleChange = (event) => { 78 | this.setState({[event.target.id]: event.target.value}); 79 | }; 80 | 81 | login = (event) => { 82 | event.preventDefault(); 83 | this.setState({isInFlight: true}); 84 | const user = new CognitoUser({ Username: this.state.username, Pool: this.props.userPool }); 85 | const authenticationData = { Username: this.state.username, Password: this.state.password }; 86 | const authenticationDetails = new AuthenticationDetails(authenticationData); 87 | this.setState({user}, () => user.authenticateUser(authenticationDetails, this)); 88 | }; 89 | 90 | render() { 91 | const {isInFlight} = this.state; 92 | const errorString = this.state.error === undefined ? '' : this.state.error.message; 93 | let userElement, formSubmit; 94 | if (this.state.passwordChangeAttributes !== undefined) { 95 | formSubmit = this.submitNewPassword; 96 | userElement = 98 | Account used for the first time.
99 | You need to change your password. 100 |
101 | 102 | } else { 103 | formSubmit = this.login; 104 | userElement = 105 | Username 106 | 112 | ; 113 | } 114 | 115 | let warning; 116 | 117 | if (errorString !== '') { 118 | warning =
{errorString}
119 | } 120 | 121 | return ( 122 |
123 |
124 | {userElement} 125 | 126 | Password 127 | 132 | 133 | 134 | {warning} 135 | 136 | 144 | 145 |
146 | You can manage Users using the User Pool in AWS Cognito. 147 |
148 |
149 | 150 |
151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /ui/src/components/navigation.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {Component, PropTypes} from 'react'; 17 | import {Navbar, Nav, NavItem} from 'react-bootstrap'; 18 | import {LinkContainer} from 'react-router-bootstrap'; 19 | import {CognitoUserPool} from 'amazon-cognito-identity-js'; 20 | 21 | interface NavigationProps { 22 | userPool: CognitoUserPool, 23 | userLogout: PropTypes.func, 24 | } 25 | 26 | class Navigation extends Component { 27 | render() { 28 | const username = this.props.userPool.getCurrentUser().getUsername(); 29 | 30 | return( 31 | 32 | 33 | 34 | The Wheel 35 | 36 | 37 | 42 | 45 | 46 | Signed in as: {username} 47 | 48 | 49 | ) 50 | } 51 | } 52 | 53 | export default Navigation; 54 | -------------------------------------------------------------------------------- /ui/src/components/notFound.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React from "react"; 17 | 18 | export default () => 19 |
20 |

Sorry, page not found!

21 |
; -------------------------------------------------------------------------------- /ui/src/components/participant_table/participant_modal.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {PropTypes, Component} from 'react'; 17 | import {Modal, Button, Form} from 'react-bootstrap'; 18 | import {ParticipantType} from '../../types'; 19 | import {validate} from 'validate.js'; 20 | 21 | validate.options = {format: 'flat'}; 22 | validate.validators.uniqueName = function(participant, participantList, key, attributes) { 23 | const participantWithName = participantList.filter(existingParticipant => existingParticipant.name === participant.name); 24 | if(participantWithName.length > 0 && participantWithName[0].id !== participant.id) { 25 | return 'Name is already taken'; 26 | } 27 | return undefined; 28 | }; 29 | 30 | interface ParticipantModalProps { 31 | isOpen: boolean; 32 | onSubmit: Function; 33 | onClose: Function; 34 | participant: ParticipantType; 35 | participantList: ParticipantType[]; 36 | } 37 | 38 | interface ParticipantModalState { 39 | participant: ParticipantType, 40 | isAdd: boolean, 41 | } 42 | 43 | const defaultParticipant = { 44 | id: '', 45 | name: '', 46 | url: '', 47 | weight: 1 48 | }; 49 | 50 | export default class ParticipantModal extends Component { 51 | constructor(props) { 52 | super(props); 53 | this.state = { 54 | participant: Object.assign({}, defaultParticipant), 55 | isAdd: false, 56 | }; 57 | this.onChange = this.onChange.bind(this); 58 | this.onSubmit = this.onSubmit.bind(this); 59 | this.modalAfterOpen = this.modalAfterOpen.bind(this); 60 | this.modalOnClose = this.modalOnClose.bind(this); 61 | } 62 | 63 | onChange(event) { 64 | this.setState({participant: Object.assign({}, this.state.participant, {[event.target.name]: event.target.value})}); 65 | } 66 | 67 | onSubmit(event) { 68 | event.preventDefault(); 69 | this.props.onSubmit(this.state.participant); 70 | this.modalOnClose(); 71 | } 72 | 73 | modalAfterOpen() { 74 | const {participant} = this.props; 75 | this.setState({isAdd: participant === undefined, participant: Object.assign({}, participant || defaultParticipant)}); 76 | } 77 | 78 | modalOnClose() { 79 | this.props.onClose(); 80 | } 81 | 82 | getErrors() { 83 | const {participantList} = this.props; 84 | const {name, url} = this.state.participant; 85 | 86 | const constraints = { 87 | name: { 88 | presence: { 89 | allowEmpty: false 90 | } 91 | }, 92 | url: { 93 | presence: { 94 | allowEmpty: false 95 | }, 96 | url: true 97 | } 98 | }; 99 | 100 | let errors = validate({name: name, url: url}, constraints) || []; 101 | const uniqueNameError = validate({participant: this.state.participant}, {participant: {uniqueName: participantList}}, {fullMessages: false}); 102 | if (uniqueNameError) { 103 | errors.push(uniqueNameError[0]); 104 | } 105 | 106 | return errors.map(error =>
{error}
); 107 | } 108 | 109 | render() { 110 | const {isOpen} = this.props; 111 | const {participant, isAdd} = this.state; 112 | 113 | const heading = isAdd ? 'Add a new participant' : 'Edit an existing participant'; 114 | const submitText = isAdd ? 'Add Participant' : 'Update Participant'; 115 | const modalStyle = { 116 | position: 'fixed', 117 | zIndex: 1040, 118 | top: 0, bottom: 0, left: 0, right: 0 119 | }; 120 | 121 | const errors = isOpen ? this.getErrors() : [
Modal is closed.
]; 122 | const isDisabled = errors.length > 0; 123 | 124 | return ( 125 |
126 | 127 |
128 | 129 | {heading} 130 | 131 | 132 |
133 | 134 | 135 |
136 |
137 | 138 | 139 |
140 |
141 | 142 |
{errors}
143 | 144 | 145 |
146 |
147 |
148 |
149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /ui/src/components/participant_table/participant_row.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {PropTypes, Component} from 'react'; 17 | import {Button, ButtonGroup, ButtonToolbar} from 'react-bootstrap'; 18 | import ParticipantModal from './participant_modal'; 19 | import ConfirmationModal from '../confirmation_modal'; 20 | import {ParticipantType} from '../../types'; 21 | import {LinkWrapper} from '../../util'; 22 | 23 | interface ParticipantRowProps { 24 | participant: ParticipantType; 25 | totalParticipantWeight: number; 26 | rig: boolean; 27 | hidden: boolean; 28 | onEdit: Function; 29 | onDelete: Function; 30 | onRig: Function; 31 | onHidden: Function; 32 | participantList: ParticipantType[]; 33 | } 34 | 35 | export default class ParticipantRow extends Component { 36 | 37 | constructor(props) { 38 | super(props); 39 | this.state = { 40 | participationModalOpen: false, 41 | confirmationModalOpen: false, 42 | }; 43 | } 44 | 45 | toggleParticipationModal = () => { 46 | this.setState({participationModalOpen: !this.state.participationModalOpen}); 47 | } 48 | 49 | toggleConfirmationModal = () => { 50 | this.setState({confirmationModalOpen: !this.state.confirmationModalOpen}); 51 | } 52 | 53 | handleUpdateParticipant = (participant: ParticipantType) => { 54 | this.props.onEdit(participant); 55 | } 56 | 57 | handleDeleteParticipant = () => { 58 | this.props.onDelete(this.props.participant); 59 | } 60 | 61 | handleRigParticipant = () => { 62 | this.props.onRig(this.props.participant); 63 | } 64 | handleHiddenRigParticipant = () => { 65 | this.props.onHidden(this.props.participant); 66 | } 67 | 68 | render(){ 69 | const {participant, totalParticipantWeight} = this.props; 70 | const {participationModalOpen, confirmationModalOpen} = this.state; 71 | 72 | return ( 73 | 74 | 75 | {participant.name} 76 | 77 | 78 | {participant.url} 79 | 80 | 81 | {(participant.weight / totalParticipantWeight * 100).toFixed(2)}% 82 | 83 | 84 | 89 | 93 | 94 | 95 | 100 | 101 | 102 | 107 | 108 | 109 | 110 | 111 | 114 | 115 | 116 | 119 | 120 | 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /ui/src/components/wheel_table/wheel_modal.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {PropTypes, Component} from 'react'; 17 | import {Modal, Button, Form} from 'react-bootstrap'; 18 | import {WheelType} from '../../types'; 19 | 20 | export interface WheelModalProps { 21 | isModalOpen: bool; 22 | wheel: WheelType | undefined; 23 | onClose: PropTypes.func; 24 | onSubmit: PropTypes.func; 25 | } 26 | 27 | interface WheelModalState { 28 | wheel: WheelType; 29 | isAdd: bool; 30 | } 31 | 32 | const defaultWheel: WheelType = { 33 | id: '', 34 | name: '', 35 | participant_count: 0, 36 | }; 37 | 38 | export default class WheelModal extends Component { 39 | constructor(props: WheelModalProps) { 40 | super(props); 41 | this.state = { 42 | wheel: Object.assign({}, defaultWheel), 43 | isAdd: false, 44 | }; 45 | this.onChange = this.onChange.bind(this); 46 | this.onSubmit = this.onSubmit.bind(this); 47 | this.modalAfterOpen = this.modalAfterOpen.bind(this); 48 | this.onClose = this.onClose.bind(this); 49 | } 50 | 51 | onSubmit(event: React.MouseEvent) { 52 | event.preventDefault(); 53 | this.props.onSubmit(this.state.wheel); 54 | this.onClose(); 55 | }; 56 | 57 | onClose() { 58 | this.props.onClose(); 59 | } 60 | 61 | onChange(event: any) { 62 | const {wheel} = this.state; 63 | this.setState({wheel: Object.assign({}, wheel, {[event.target.name]: event.target.value})}); 64 | }; 65 | 66 | modalAfterOpen() { 67 | const {wheel} = this.props; 68 | this.setState( 69 | { 70 | isAdd: wheel === undefined, 71 | wheel: Object.assign({}, wheel || defaultWheel) 72 | 73 | }); 74 | }; 75 | 76 | render() { 77 | const {isModalOpen} = this.props; 78 | const {wheel, isAdd} = this.state; 79 | 80 | const heading: string = isAdd ? 'Add a new wheel' : 'Edit an existing wheel'; 81 | const submitText: string = isAdd ? 'Add Wheel' : 'Update Wheel'; 82 | const modalStyle = { 83 | position: 'fixed', 84 | zIndex: 1040, 85 | top: 0, bottom: 0, left: 0, right: 0 86 | }; 87 | 88 | return ( 89 |
90 | 96 | 97 | {heading} 98 | 99 | 100 |
101 |
102 | 103 | 110 |
111 | 112 | 116 | 123 | 124 |
125 |
126 |
127 |
128 | ); 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /ui/src/components/wheel_table/wheel_row.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {PropTypes, Component} from 'react'; 17 | import {LinkWrapper} from '../../util'; 18 | import WheelModal from './wheel_modal'; 19 | import ConfirmationModal from '../confirmation_modal'; 20 | import {Button, ButtonGroup, ButtonToolbar} from 'react-bootstrap'; 21 | import {formatDateTime} from '../../util'; 22 | import {WheelType} from '../../types'; 23 | 24 | export interface WheelRowState { 25 | isWheelModalOpen: bool; 26 | isConfirmationModalOpen: bool; 27 | } 28 | 29 | export interface WheelRowProps { 30 | wheel: WheelType; 31 | onEdit: PropTypes.func; 32 | onDelete: PropTypes.func; 33 | } 34 | 35 | export default class WheelRow extends Component { 36 | constructor(props: WheelRowProps) { 37 | super(props); 38 | this.state = { 39 | isWheelModalOpen: false, 40 | isConfirmationModalOpen: false, 41 | }; 42 | } 43 | 44 | toggleWheelModal = () => { 45 | this.setState({isWheelModalOpen: !this.state.isWheelModalOpen}); 46 | } 47 | 48 | toggleConfirmationModal = () => { 49 | this.setState({isConfirmationModalOpen: !this.state.isConfirmationModalOpen}); 50 | } 51 | 52 | handleWheelEdit = (wheel: WheelType) => { 53 | this.props.onEdit(wheel); 54 | }; 55 | 56 | handleWheelDelete = () => { 57 | this.props.onDelete(this.props.wheel); 58 | }; 59 | 60 | render() { 61 | const {wheel} = this.props; 62 | const {isWheelModalOpen, isConfirmationModalOpen} = this.state; 63 | 64 | return ( 65 | 66 | 67 | {wheel.name} 68 | 69 | 70 | {wheel.participant_count} 71 | 72 | 73 | {formatDateTime(wheel.updated_at, true)} 74 | 75 | 76 | {formatDateTime(wheel.created_at, true)} 77 | 78 | 79 | 84 | 90 | 91 | 92 | 99 | 100 | 101 | 102 | 105 | 106 | 107 | 108 | 114 | 115 | 116 | 117 | 118 | ); 119 | } 120 | } -------------------------------------------------------------------------------- /ui/src/components/wheel_table/wheel_table.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {PropTypes, Component} from 'react'; 17 | import connect from 'react-redux-fetch'; 18 | import WheelRow from './wheel_row'; 19 | import WheelModal from './wheel_modal'; 20 | import {Well, Table, PageHeader, Button} from 'react-bootstrap'; 21 | import {WheelType} from '../../types'; 22 | import '../../static_content/favicon.ico'; 23 | import {apiURL} from '../../util'; 24 | 25 | interface WheelTableState { 26 | isWheelModalOpen: bool; 27 | } 28 | 29 | interface WheelTableProps { 30 | wheelsFetch: PropTypes.object; 31 | createWheelFetch: PropTypes.object; 32 | updateWheelFetch: PropTypes.object; 33 | deleteWheelFetch: PropTypes.object; 34 | dispatchWheelsGet: PropTypes.func; 35 | dispatchCreateWheelPost: PropTypes.func; 36 | dispatchUpdateWheelPut: PropTypes.func; 37 | dispatchDeleteWheelDelete: PropTypes.func; 38 | } 39 | 40 | export class WheelTable extends Component { 41 | 42 | existing: WheelType; 43 | 44 | constructor(props: WheelTableProps) { 45 | super(props); 46 | this.state = { 47 | isWheelModalOpen: false, 48 | create: false, 49 | edit: false, 50 | delete: false, 51 | }; 52 | this.existing = undefined; 53 | } 54 | 55 | componentWillMount() { 56 | this.props.dispatchWheelsGet(); 57 | } 58 | 59 | toggleWheelModal = () => { 60 | this.setState({isWheelModalOpen: !this.state.isWheelModalOpen}); 61 | }; 62 | 63 | handleWheelAdd = (wheel: WheelType) => { 64 | this.props.dispatchCreateWheelPost(wheel); 65 | this.setState({create: true}); 66 | }; 67 | 68 | handleWheelEdit = (wheel: WheelType) => { 69 | this.props.dispatchUpdateWheelPut(wheel); 70 | this.setState({edit: true}); 71 | }; 72 | 73 | handleWheelDelete = (wheel: WheelType) => { 74 | this.props.dispatchDeleteWheelDelete(wheel.id); 75 | this.setState({delete: true}); 76 | }; 77 | 78 | componentDidUpdate() { 79 | let updates = {}; 80 | if (this.state.create && this.props.createWheelFetch.fulfilled) { 81 | updates.create = false; 82 | } 83 | if (this.state.edit && this.props.updateWheelFetch.fulfilled) { 84 | updates.edit = false; 85 | } 86 | if (this.state.delete && this.props.deleteWheelFetch.fulfilled) { 87 | updates.delete = false; 88 | } 89 | if (Object.keys(updates).length > 0) { 90 | this.setState(updates); 91 | this.props.dispatchWheelsGet(); 92 | } 93 | } 94 | 95 | render() { 96 | const {wheelsFetch} = this.props; 97 | if (wheelsFetch.rejected) { 98 | return (
Oops... Could not fetch the wheels data!
); 99 | } 100 | if (wheelsFetch.fulfilled) { 101 | this.existing = wheelsFetch.value; 102 | } 103 | if (this.existing === undefined) { 104 | return (
Loading...
); 105 | } 106 | 107 | const {isWheelModalOpen} = this.state; 108 | const wheels = JSON.parse(JSON.stringify(this.existing.Items)).sort((a, b) => a.name.localeCompare(b.name)); 109 | let wheelRows = wheels.map( 110 | item => ); 111 | 112 | return ( 113 |
114 |
115 | 116 | 117 |
118 | List of available Wheels 119 | 126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | {wheelRows} 140 | 141 |
Wheel NameNumber of ParticipantsLast UpdatedCreated AtOperations
142 |
143 |
144 | 149 |
150 | ); 151 | } 152 | } 153 | 154 | export default connect([ 155 | { 156 | resource: 'wheels', 157 | method: 'get', 158 | request: { 159 | url: apiURL('wheel'), 160 | } 161 | }, 162 | { 163 | resource: 'createWheel', 164 | method: 'post', 165 | request: (wheel) => ({ 166 | url: apiURL('wheel'), 167 | body: JSON.stringify(wheel) 168 | }) 169 | }, 170 | { 171 | resource: 'updateWheel', 172 | method: 'put', 173 | request: (wheel) => ({ 174 | url: apiURL(`wheel/${wheel.id}`), 175 | body: JSON.stringify(wheel) 176 | }) 177 | }, 178 | { 179 | resource: 'deleteWheel', 180 | method: 'delete', 181 | request: (wheelId) => ({ 182 | url: apiURL(`wheel/${wheelId}`), 183 | meta: { 184 | removeFromList: { 185 | idName: 'id', 186 | id: wheelId, 187 | } 188 | } 189 | }) 190 | } 191 | ])(WheelTable); 192 | -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | The Wheel 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /ui/src/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | declare var window: Window & { devToolsExtension: any, __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any }; 17 | import React from 'react'; 18 | import {render} from 'react-dom'; 19 | import {Provider} from 'react-redux'; 20 | import {createLogger} from 'redux-logger'; 21 | import {createStore, applyMiddleware, combineReducers, compose} from 'redux'; 22 | import {middleware as fetchMiddleware, reducer as repository} from 'react-redux-fetch'; 23 | import 'bootstrap/dist/css/bootstrap.css'; 24 | import 'bootstrap/dist/css/bootstrap-theme.css'; 25 | import 'font-awesome/css/font-awesome.css'; 26 | 27 | import App from './components/app'; 28 | 29 | 30 | /* 31 | Integration of Redux Store Chrome debugger: 32 | https://github.com/zalmoxisus/redux-devtools-extension 33 | */ 34 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 35 | 36 | const middleWare = composeEnhancers(applyMiddleware( 37 | fetchMiddleware, // lets us dispatch() functions 38 | createLogger({}) // neat middleware that logs actions and the state before and after (super useful for debugging) 39 | )); 40 | 41 | const store = createStore(combineReducers({repository}), undefined, middleWare); 42 | 43 | render( 44 | 45 | 46 | , 47 | document.getElementById('app') 48 | ); 49 | -------------------------------------------------------------------------------- /ui/src/static_content/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-ops-wheel/34319e4ddffee51635a837feec0b3b4076acd16b/ui/src/static_content/favicon.ico -------------------------------------------------------------------------------- /ui/src/static_content/wheel_click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-ops-wheel/34319e4ddffee51635a837feec0b3b4076acd16b/ui/src/static_content/wheel_click.mp3 -------------------------------------------------------------------------------- /ui/src/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | body { 17 | background-color: #FAFAFA; 18 | } 19 | 20 | .pageRoot { 21 | border: 1px solid #FAFAFA; 22 | } 23 | 24 | .title-text { 25 | margin: auto; 26 | width: 50%; 27 | } 28 | 29 | .wheelNavButtonGroup { 30 | font-family: Arial; 31 | position: absolute; 32 | top: 2%; 33 | left: 1%; 34 | } 35 | 36 | #app .well { 37 | padding: 8px 12px; 38 | background-color: #eaeded; 39 | border-radius: 4px; 40 | background-image: none; 41 | } 42 | 43 | #app .page-header { 44 | padding-top: 7.5px; 45 | padding-bottom: 7.5px; 46 | margin: 0 0 18px; 47 | border-bottom: 1px solid #879196; 48 | } 49 | 50 | thead th { 51 | text-transform:uppercase; 52 | text-align:left; 53 | color: #545b64; 54 | font-size: 14px; 55 | } 56 | 57 | button:focus {outline:0;} 58 | 59 | .title { 60 | background-color: #FAFAFA; 61 | text-align: center; 62 | font-family: Helvetica; 63 | color: #232F3E; 64 | } 65 | 66 | .notification { 67 | float: right; 68 | } 69 | 70 | .errorNotification { 71 | color: #df3312; 72 | } 73 | 74 | .tableHeader { 75 | font-size: 20px; 76 | color: #232F3E; 77 | } 78 | -------------------------------------------------------------------------------- /ui/src/types.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | export type WheelType = { 17 | id?: string; 18 | name: string; 19 | participant_count?: number; 20 | rigging?: { 21 | hidden: boolean, 22 | participant_id: string, 23 | }; 24 | created_at?: string, 25 | updated_at?: string, 26 | } 27 | 28 | export type ParticipantType = { 29 | id?: string; 30 | name: string; 31 | url: string; 32 | wheel_id: string; 33 | created_at?: string; 34 | updated_at?: string; 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/util.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import React, {Component} from 'react'; 17 | import moment from 'moment'; 18 | import * as moment_tz from 'moment-timezone'; 19 | import {Link} from 'react-router-dom'; 20 | 21 | export const DATE_FORMAT: string = 'YYYY-MM-DD HH:mm:ss'; 22 | export function formatDateTime(timestamp: string, withTimeZone: boolean) { 23 | if (withTimeZone) { 24 | const dateTimeFormat = 'YYYY-MM-DD HH:mm:ss z'; 25 | return moment_tz.tz(timestamp, moment_tz.tz.guess()).format(dateTimeFormat); 26 | } else { 27 | return moment(timestamp).format(DATE_FORMAT); 28 | } 29 | } 30 | 31 | export const WHEEL_COLORS = [ 32 | '#FF9900', 33 | '#007dbc', 34 | '#ec7211', 35 | '#FFFFFF', 36 | '#6aaf35', 37 | '#aab7b8', 38 | '#df3312', 39 | '#545b64', 40 | '#eaeded', 41 | '#eb5f07', 42 | '#FAFAFA', 43 | '#00a1c9', 44 | '#F2F4F4', 45 | '#1e8900', 46 | '#d5dbdb', 47 | '#ff5746', 48 | ]; 49 | 50 | // Testing URLs need to be absolute 51 | export const apiURL = (urlSuffix) => { 52 | const urlPrefix = (process.env.NODE_ENV === 'test') ? 'http://localhost' : ''; 53 | return (`${urlPrefix}/app/api/${urlSuffix}`); 54 | }; 55 | 56 | export const staticURL = (urlSuffix) => { 57 | const urlPrefix = (process.env.NODE_ENV === 'test') ? 'http://localhost' : ''; 58 | return (`${urlPrefix}/app/static/${urlSuffix}`); 59 | }; 60 | 61 | /* This is a wrapper around Link to disable Links while testing and apply a local route prefix. 62 | cannot exist outside of a router context (it triggers an Invariant), but creating a router 63 | context makes it very difficult to access the internals of the object via enzyme as it is wrapped by the 64 | . 65 | */ 66 | export class LinkWrapper extends Component { 67 | render () { 68 | let link: any; 69 | let props: Object = Object.assign({}, this.props); 70 | 71 | if (props.remote !== true) 72 | props.to = `/app/${props.to}`; 73 | 74 | if ('remote' in props) 75 | delete props.remote; 76 | 77 | if (process.env.NODE_ENV === 'test') 78 | link =
{props.to}`} {props.children}
; 79 | else 80 | /* istanbul ignore next */ 81 | link = {props.children}; 82 | 83 | return ( 84 |
85 | {link} 86 |
87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ui/test/components/confirmation_modal.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import ConfirmationModal from '../../src/components/confirmation_modal'; 20 | import '../globals'; 21 | import {Button} from 'react-bootstrap'; 22 | import {mount, shallow} from 'enzyme'; 23 | 24 | 25 | describe('ConfirmationModal', function() { 26 | // sinon.sandbox lets us group these spies together and reset them after every test 27 | const sandbox = sinon.createSandbox(); 28 | afterEach(() =>{ 29 | sandbox.reset(); 30 | }); 31 | 32 | const props = { 33 | isModalOpen: true, 34 | message: 'test_message', 35 | closeModal: sandbox.spy(), 36 | onConfirm: sandbox.spy(), 37 | }; 38 | 39 | it('Should mount and render with modal open in edit mode', () => { 40 | const wrapper = mount(); 41 | expect(wrapper.html()).to.contain('message'); 42 | }); 43 | 44 | it('Should call props.onSubmit() and props.onClose() upon submit', () => { 45 | const wrapper = shallow(); 46 | // Call modalAfterOpen to update state 47 | wrapper.find(Button).at(1).simulate('click', {preventDefault: () => {}}); 48 | expect(props.onConfirm.calledOnce).to.be.true; 49 | expect(props.closeModal.calledOnce).to.be.true; 50 | }); 51 | 52 | }); -------------------------------------------------------------------------------- /ui/test/components/login.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import Login from '../../src/components/login'; 20 | import '../globals'; 21 | import {mount} from 'enzyme'; 22 | import {CognitoUserPool, CognitoUser} from "amazon-cognito-identity-js"; 23 | import {Button} from 'react-bootstrap'; 24 | 25 | 26 | describe('Login', function() { 27 | 28 | const sandbox = sinon.createSandbox(); 29 | 30 | afterEach(() => { 31 | sandbox.reset(); 32 | }); 33 | 34 | const props = { 35 | userHasAuthenticated: sandbox.spy(), 36 | userPool: new CognitoUserPool({ 37 | UserPoolId: 'test_user_pool_id', 38 | ClientId: 'test_client_id', 39 | }), 40 | }; 41 | 42 | it('Should mount and render', () => { 43 | const wrapper = mount(); 44 | expect(wrapper.html()).to.contain('You can manage Users using the User Pool in AWS Cognito.'); 45 | }); 46 | 47 | it('Should set state and call userHasAuthenticated upon onSuccess()', () => { 48 | const wrapper = mount(); 49 | wrapper.instance().setState({error: {message: 'test'}, passwordChangeAttributes: {}, isInFlight: true}); 50 | wrapper.instance().onSuccess(); 51 | expect(wrapper.instance().state.error).to.equal(undefined); 52 | expect(wrapper.instance().state.passwordChangeAttributes).to.equal(undefined); 53 | expect(wrapper.instance().state.isInFlight).to.be.false; 54 | expect(props.userHasAuthenticated.calledWith(true)).to.be.true; 55 | }); 56 | 57 | it('Should set state upon onFailure()', () => { 58 | const wrapper = mount(); 59 | wrapper.instance().setState({isInFlight: true}); 60 | wrapper.instance().onFailure(); 61 | expect(wrapper.instance().state.isInFlight).to.be.false; 62 | }); 63 | 64 | it('Should set state upon newPasswordRequired()', () => { 65 | const testUserAttribs = { 66 | email_verified: true, 67 | somethingElse: 'test', 68 | }; 69 | 70 | const wrapper = mount(); 71 | wrapper.instance().setState({error: {}, password: 'test', isInFlight: true }); 72 | wrapper.instance().newPasswordRequired(testUserAttribs); 73 | let expectedUserAttribs = JSON.parse(JSON.stringify(testUserAttribs)); 74 | delete expectedUserAttribs.email_verified; 75 | expect(wrapper.instance().state.passwordChangeAttributes).to.deep.equal(expectedUserAttribs); 76 | expect(wrapper.instance().state.error).to.equal(undefined); 77 | expect(wrapper.instance().state.password).to.equal(''); 78 | expect(wrapper.instance().state.isInFlight).to.be.false; 79 | }); 80 | 81 | it('Should set state and call completeNewPasswordChallenge upon Login button submit with password not yet set', 82 | () => { 83 | const testPWChangeAttribs = { 84 | somethingElse: 'test', 85 | }; 86 | const testUser = { 87 | completeNewPasswordChallenge: sinon.spy(), 88 | }; 89 | 90 | const wrapper = mount(); 91 | wrapper.instance().setState( 92 | { 93 | passwordChangeAttributes: testPWChangeAttribs, 94 | user: testUser, 95 | }); 96 | wrapper.find('[type="username"]').at(1).simulate('change', {target: {id: 'username', value: 'test_username'}}); 97 | wrapper.find('[type="password"]').at(1).simulate('change', {target: {id: 'password', value: 'test_password'}}); 98 | wrapper.find(Button).simulate('submit', {preventDefault: () => {}}); 99 | expect(wrapper.instance().state.isInFlight).to.be.true; 100 | expect(testUser.completeNewPasswordChallenge.calledWith('test_password', testPWChangeAttribs, wrapper.instance())) 101 | .to.be.true; 102 | }); 103 | 104 | it('Should set state upon handleChange()', () => { 105 | const wrapper = mount(); 106 | wrapper.instance().handleChange({target: {id: 'username', value: 'test_username'}}); 107 | expect(wrapper.instance().state.username).to.equal('test_username'); 108 | }); 109 | 110 | it('Should set state appropriately on Login button submit with password already set', () => { 111 | const testUser = { 112 | completeNewPasswordChallenge: sinon.spy(), 113 | }; 114 | const expectedUser = new CognitoUser({Username: 'test_username', Pool: props.userPool}); 115 | 116 | const wrapper = mount(); 117 | wrapper.instance().setState({user: testUser}); 118 | wrapper.find('[type="username"]').at(1).simulate('change', {target: {id: 'username', value: 'test_username'}}); 119 | wrapper.find('[type="password"]').at(1).simulate('change', {target: {id: 'password', value: 'test_password'}}); 120 | wrapper.find(Button).simulate('submit', {preventDefault: () => {}}); 121 | expect(wrapper.instance().state.isInFlight).to.be.true; 122 | expect(wrapper.instance().state.user).to.deep.equal(expectedUser); 123 | }); 124 | 125 | it('Should render error message if set', () => { 126 | const wrapper = mount(); 127 | wrapper.instance().setState({error: {message: 'test_error_message'}}); 128 | expect(wrapper.html()).to.contain('test_error_message'); 129 | }); 130 | }); -------------------------------------------------------------------------------- /ui/test/components/navigation.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import Navigation from '../../src/components/navigation'; 20 | import {mountWithRoute} from '../globals'; 21 | import '../globals'; 22 | 23 | describe('Navigation', function() { 24 | 25 | const sandbox = sinon.createSandbox(); 26 | 27 | afterEach(() => { 28 | sandbox.reset(); 29 | }); 30 | 31 | const userPoolStub = sandbox.stub(); 32 | const currentUserStub = sandbox.stub(); 33 | userPoolStub.returns({getUsername: currentUserStub}); 34 | currentUserStub.returns('test_user'); 35 | 36 | const props = { 37 | userPool: { 38 | getCurrentUser: userPoolStub, 39 | }, 40 | userLogout: sandbox.spy(), 41 | }; 42 | 43 | it('Should mount and render', () => { 44 | const wrapper = mountWithRoute(); 45 | expect(wrapper.html()).to.contain('test_user'); 46 | }); 47 | }); -------------------------------------------------------------------------------- /ui/test/components/notfound.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import {mount} from 'enzyme'; 19 | import '../globals'; 20 | import NotFound from '../../src/components/notFound'; 21 | 22 | describe('NotFound', function() { 23 | 24 | it('Should mount and render', () => { 25 | const wrapper = mount(); 26 | expect(wrapper.html()).to.contain('Sorry, page not found!'); 27 | }); 28 | }); -------------------------------------------------------------------------------- /ui/test/components/participant_modal.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import ParticipantModal from '../../src/components/participant_table/participant_modal'; 20 | import '../globals'; 21 | import {shimData} from '../shim_data'; 22 | import {Button} from 'react-bootstrap'; 23 | import {mount, shallow} from 'enzyme'; 24 | 25 | 26 | describe('ParticipantModal', function() { 27 | // sinon.sandbox lets us group these spies together and reset them after every test 28 | const sandbox = sinon.createSandbox(); 29 | afterEach(() =>{ 30 | sandbox.reset(); 31 | }); 32 | 33 | const participants = shimData.participants.filter((e) => e.wheel_id === shimData.wheels[0].id); 34 | 35 | const props = { 36 | isOpen: true, 37 | onSubmit: sandbox.spy(), 38 | onClose: sandbox.spy(), 39 | participant: participants[0], 40 | participantList: participants, 41 | }; 42 | 43 | const defaultParticipant = { 44 | id: '', 45 | name: '', 46 | url: '', 47 | weight: 1 48 | }; 49 | 50 | it('Should mount and render with modal open in edit mode', () => { 51 | const wrapper = mount(); 52 | expect(wrapper.instance().state.isAdd).to.be.false; 53 | expect(wrapper.instance().state.participant).to.deep.equal(participants[0]); 54 | }); 55 | 56 | it('Should mount and render with modal open in add mode', () => { 57 | const wrapper = mount(); 58 | expect(wrapper.instance().state.isAdd).to.be.true; 59 | expect(wrapper.instance().state.participant).to.deep.equal(defaultParticipant); 60 | }); 61 | 62 | it('Should call props.onSubmit() and props.onClose() upon submit', () => { 63 | const wrapper = shallow(); 64 | // Call modalAfterOpen to update state 65 | wrapper.instance().modalAfterOpen(); 66 | wrapper.find(Button).at(1).simulate('click', {preventDefault: () => {}}); 67 | expect(props.onSubmit.calledWith(participants[0])).to.be.true; 68 | expect(props.onClose.calledOnce).to.be.true; 69 | }); 70 | 71 | it('Should generate errors for name and url on call to getErrors', () => { 72 | const wrapper = shallow(); 73 | wrapper.instance().modalAfterOpen(); 74 | expect(wrapper.instance().state.isAdd).to.be.true; 75 | let errors = wrapper.instance().getErrors().map((e) => e.props.children); 76 | expect(errors[0]).to.equal('Name can\'t be blank'); 77 | expect(errors[1]).to.equal('Url can\'t be blank'); 78 | expect(errors[2]).to.equal('Url is not a valid url'); 79 | }); 80 | 81 | it('Should call generate error for duplicate name on call to getErrors', () => { 82 | const testParticipant = Object.assign({}, participants[0], {id: 'test_id'}); 83 | const wrapper = shallow(); 84 | wrapper.instance().modalAfterOpen(); 85 | expect(wrapper.instance().state.isAdd).to.be.true; 86 | wrapper.instance().state.participant = testParticipant; 87 | let errors = wrapper.instance().getErrors().map((e) => e.props.children); 88 | expect(errors[0]).to.equal('Name is already taken'); 89 | }); 90 | 91 | it('Should update participant state correctly when onChange is called', () => { 92 | const expectedParticipant = Object.assign({}, participants[0], {name: 'test_updated_name', url: 'http://testupdatedurl.com'}); 93 | const wrapper = shallow(); 94 | wrapper.instance().modalAfterOpen(); 95 | expect(wrapper.instance().state.isAdd).to.be.false; 96 | wrapper.find("[name='name']").simulate('change', {target: {name: 'name', value: 'test_updated_name'}}); 97 | wrapper.find("[name='url']").simulate('change', {target: {name: 'url', value: 'http://testupdatedurl.com'}}); 98 | expect(wrapper.instance().state.participant).to.deep.equal(expectedParticipant); 99 | }); 100 | }); -------------------------------------------------------------------------------- /ui/test/components/participant_row.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import ParticipantRow from '../../src/components/participant_table/participant_row'; 20 | import '../globals'; 21 | import {shimData} from '../shim_data'; 22 | import {Button} from 'react-bootstrap'; 23 | import {mount} from 'enzyme'; 24 | 25 | 26 | describe('ParticipantRow', function() { 27 | const sandbox = sinon.createSandbox(); 28 | afterEach(() => { 29 | sandbox.reset(); 30 | }); 31 | 32 | const participants = shimData.participants.filter((e) => e.wheel_id === shimData.wheels[0].id); 33 | const participant = participants[0]; 34 | 35 | const props = { 36 | participant: participant, 37 | rig: false, 38 | hidden: false, 39 | onEdit: sandbox.spy(), 40 | onDelete: sandbox.spy(), 41 | onRig: sandbox.spy(), 42 | onHidden: sandbox.spy(), 43 | participantList: participants, 44 | }; 45 | 46 | it('Should render and display wheel name', () => { 47 | const wrapper = mount(); 48 | expect(wrapper.html()).to.contain(participant.name); 49 | }); 50 | 51 | it('Should toggle participationModalOpen state on click of Edit button', () => { 52 | const wrapper = mount(); 53 | expect(wrapper.instance().state.participationModalOpen).to.be.false; 54 | wrapper.find(Button).at(0).simulate('click'); 55 | expect(wrapper.instance().state.participationModalOpen).to.be.true; 56 | }); 57 | 58 | it('Should toggle confirmationModalOpen state on click of delete button', () => { 59 | const wrapper = mount(); 60 | expect(wrapper.instance().state.confirmationModalOpen).to.be.false; 61 | wrapper.find(Button).at(1).simulate('click'); 62 | expect(wrapper.instance().state.confirmationModalOpen).to.be.true; 63 | }); 64 | 65 | it('Should call handleRigParticipant() on click of rig radio button', () => { 66 | const wrapper = mount(); 67 | wrapper.find("[type='radio']").simulate('change', {target: {checked: true}}); 68 | expect(props.onRig.calledWith(participant)).to.be.true; 69 | }); 70 | 71 | it('Should call handleHiddenRigParticipant() on click of hidden checkbox', () => { 72 | const wrapper = mount(); 73 | wrapper.find("[type='checkbox']").simulate('change', {target: {checked: true}}); 74 | expect(props.onHidden.calledWith(participant)).to.be.true; 75 | }); 76 | 77 | it('Should call props.onEdit upon call to handleUpdateParticipant()', () => { 78 | const wrapper = mount(); 79 | wrapper.instance().handleUpdateParticipant(participant); 80 | expect(props.onEdit.calledWith(participant)).to.be.true; 81 | }); 82 | 83 | it('Should call props.onDelete upon call to handleDeleteParticipant()', () => { 84 | const wrapper = mount(); 85 | wrapper.instance().handleDeleteParticipant(); 86 | expect(props.onDelete.calledWith(participant)).to.be.true; 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /ui/test/components/wheel_modal.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import WheelModal from '../../src/components/wheel_table/wheel_modal'; 20 | import '../globals'; 21 | import {shimData} from '../shim_data'; 22 | import {Button} from 'react-bootstrap'; 23 | import {mount, shallow} from 'enzyme'; 24 | 25 | 26 | describe('WheelModal', function() { 27 | // sinon.sandbox lets us group these spies together and reset them after every test 28 | const sandbox = sinon.createSandbox(); 29 | afterEach(() =>{ 30 | sandbox.reset(); 31 | }); 32 | 33 | const wheel = shimData.wheels[0]; 34 | 35 | const props = { 36 | isModalOpen: true, 37 | wheel: wheel, 38 | onClose: sandbox.spy(), 39 | onSubmit: sandbox.spy(), 40 | }; 41 | 42 | it('Should mount and render with modal open in edit mode', () => { 43 | const wrapper = mount(); 44 | expect(wrapper.instance().state.isAdd).to.be.false; 45 | }); 46 | 47 | it('Should mount and render with modal open', () => { 48 | const wrapper = mount(); 49 | expect(wrapper.instance().state.isAdd).to.be.true; 50 | }); 51 | 52 | it('Should call props.onSubmit() and props.onClose() upon submit', () => { 53 | const wrapper = shallow(); 54 | // Call modalAfterOpen to update state 55 | wrapper.instance().modalAfterOpen(); 56 | wrapper.find(Button).at(1).simulate('click', {preventDefault: () => {}}); 57 | expect(props.onSubmit.calledWith(wheel)).to.be.true; 58 | expect(props.onClose.calledOnce).to.be.true; 59 | }); 60 | 61 | it('Should update participant state correctly when onChange is called', () => { 62 | const expectedWheel = Object.assign({}, wheel, {name: 'test_updated_name'}); 63 | const wrapper = shallow(); 64 | wrapper.instance().modalAfterOpen(); 65 | expect(wrapper.instance().state.isAdd).to.be.false; 66 | wrapper.find("[name='name']").simulate('change', {target: {name: 'name', value: 'test_updated_name'}}); 67 | expect(wrapper.instance().state.wheel).to.deep.equal(expectedWheel); 68 | }); 69 | }); -------------------------------------------------------------------------------- /ui/test/components/wheel_row.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import WheelRow from '../../src/components/wheel_table/wheel_row'; 20 | import '../globals'; 21 | import {shimData} from '../shim_data'; 22 | import {Button} from 'react-bootstrap'; 23 | import {mount} from 'enzyme'; 24 | 25 | 26 | describe('WheelRow', function() { 27 | const sandbox = sinon.createSandbox(); 28 | afterEach(() => { 29 | sandbox.reset(); 30 | }); 31 | 32 | const wheel = shimData.wheels[0]; 33 | 34 | const props = { 35 | wheel: wheel, 36 | onEdit: sandbox.spy(), 37 | onDelete: sandbox.spy(), 38 | }; 39 | 40 | it('Should render and display wheel name', () => { 41 | const wrapper = mount(); 42 | expect(wrapper.html()).to.contain(wheel.name); 43 | }); 44 | 45 | it('Should toggle isWheelModalOpen state on click of Edit button', () => { 46 | const wrapper = mount(); 47 | expect(wrapper.instance().state.isWheelModalOpen).to.be.false; 48 | wrapper.find(Button).at(0).simulate('click'); 49 | expect(wrapper.instance().state.isWheelModalOpen).to.be.true; 50 | }); 51 | 52 | it('Should toggle isConfirmationModalOpen state on click of Delete button', () => { 53 | const wrapper = mount(); 54 | expect(wrapper.instance().state.isConfirmationModalOpen).to.be.false; 55 | wrapper.find(Button).at(2).simulate('click'); 56 | expect(wrapper.instance().state.isConfirmationModalOpen).to.be.true; 57 | }); 58 | 59 | it('Should call props.onEdit upon call to handleWheelEdit()', () => { 60 | const wrapper = mount(); 61 | wrapper.instance().handleWheelEdit(wheel); 62 | expect(props.onEdit.calledWith(wheel)).to.be.true; 63 | }); 64 | 65 | it('Should call props.onDelete upon call to handleWheelDelete()', () => { 66 | const wrapper = mount(); 67 | wrapper.instance().handleWheelDelete(wheel); 68 | expect(props.onDelete.calledWith(wheel)).to.be.true; 69 | }); 70 | 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /ui/test/components/wheel_table.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import * as React from 'react'; 17 | import {expect} from 'chai'; 18 | import * as sinon from 'sinon'; 19 | import {mountWithRoute, shallowWithStore} from '../globals'; 20 | import ConnectedWheelTable, {WheelTable} from '../../src/components/wheel_table/wheel_table'; 21 | import '../globals'; 22 | import {shimData} from '../shim_data'; 23 | import {Button} from 'react-bootstrap'; 24 | 25 | 26 | describe('WheelTable', function() { 27 | const shallowProps = { 28 | wheelsFetch: { 29 | fulfilled: true, 30 | rejected: false, 31 | pending: false, 32 | value: {Items: shimData.wheels}, 33 | }, 34 | }; 35 | 36 | const sandbox = sinon.createSandbox(); 37 | const dispatchProps = { 38 | dispatchWheelsGet: sandbox.spy(), 39 | dispatchCreateWheelPost: sandbox.spy(), 40 | dispatchUpdateWheelPut: sandbox.spy(), 41 | dispatchDeleteWheelDelete: sandbox.spy(), 42 | }; 43 | afterEach(() => { 44 | sandbox.reset(); 45 | }); 46 | 47 | it('Should render before and after loading completely', (done) => { 48 | const wrapper = mountWithRoute(); 49 | // The wheel table should be be loading when initially mounted 50 | expect(wrapper.find('div').html()).to.contain('Loading...'); 51 | 52 | // After 50ms we should have retrieved wheels data from the nocks 53 | setTimeout(() => { 54 | expect(wrapper.find('div').html()).to.contain('wheel_name_0'); 55 | expect(wrapper.find('div').html()).to.contain('wheel_name_1'); 56 | done(); 57 | }, 50); 58 | }); 59 | 60 | it('Should set isWheelModalOpen to true when Add New Wheel button is clicked', () => { 61 | 62 | const wrapper = shallowWithStore(); 63 | expect(wrapper.find('.pageRoot').html()).to.contain('wheel_name_1'); 64 | wrapper.find(Button).simulate('click'); 65 | expect(wrapper.instance().state.isWheelModalOpen).to.be.true; 66 | }); 67 | 68 | it('Should dispatch a post upon handleWheelAdd, then update state properly on componentDidUpdate', 69 | () => { 70 | const testCreateWheel = { 71 | name: 'test_create_wheel', 72 | } 73 | const postProps = { 74 | createWheelFetch: { 75 | fulfilled: false, 76 | pending: false, 77 | rejected: false, 78 | } 79 | }; 80 | 81 | const wrapper = shallowWithStore( 82 | ); 83 | expect(wrapper.html()).to.contain('wheel_name_0'); 84 | wrapper.instance().handleWheelAdd(testCreateWheel); 85 | expect(dispatchProps.dispatchCreateWheelPost.calledWith(testCreateWheel)).to.be.true; 86 | expect(wrapper.instance().state.create).to.be.true; 87 | 88 | wrapper.setProps({createWheelFetch: {fulfilled: true}}) 89 | wrapper.instance().componentDidUpdate(); 90 | expect(dispatchProps.dispatchWheelsGet.calledTwice).to.be.true; 91 | expect(wrapper.instance().state.create).to.be.false; 92 | }); 93 | 94 | it('Should dispatch a post upon handleWheelAdd, then update state properly on componentDidUpdate', 95 | () => { 96 | const testWheel = { 97 | name: 'test_create_wheel', 98 | } 99 | const postProps = { 100 | createWheelFetch: { 101 | fulfilled: false, 102 | pending: false, 103 | rejected: false, 104 | } 105 | }; 106 | 107 | const wrapper = shallowWithStore( 108 | ); 109 | expect(wrapper.html()).to.contain('wheel_name_0'); 110 | wrapper.instance().handleWheelAdd(testWheel); 111 | expect(dispatchProps.dispatchCreateWheelPost.calledWith(testWheel)).to.be.true; 112 | expect(wrapper.instance().state.create).to.be.true; 113 | 114 | wrapper.setProps({createWheelFetch: {fulfilled: true}}) 115 | wrapper.instance().componentDidUpdate(); 116 | expect(dispatchProps.dispatchWheelsGet.calledTwice).to.be.true; 117 | expect(wrapper.instance().state.create).to.be.false; 118 | }); 119 | 120 | it('Should dispatch a put upon handleWheelEdit, then update state properly on componentDidUpdate', 121 | () => { 122 | const testWheel = { 123 | name: 'test_edit_wheel', 124 | id: shallowProps.wheelsFetch.value.Items[0].id, 125 | } 126 | const postProps = { 127 | updateWheelFetch: { 128 | fulfilled: false, 129 | pending: false, 130 | rejected: false, 131 | } 132 | }; 133 | 134 | const wrapper = shallowWithStore( 135 | ); 136 | expect(wrapper.html()).to.contain('wheel_name_0'); 137 | wrapper.instance().handleWheelEdit(testWheel); 138 | expect(dispatchProps.dispatchUpdateWheelPut.calledWith(testWheel)).to.be.true; 139 | expect(wrapper.instance().state.edit).to.be.true; 140 | 141 | wrapper.setProps({updateWheelFetch: {fulfilled: true}}) 142 | wrapper.instance().componentDidUpdate(); 143 | expect(dispatchProps.dispatchWheelsGet.calledTwice).to.be.true; 144 | expect(wrapper.instance().state.edit).to.be.false; 145 | }); 146 | 147 | it('Should dispatch a delete upon handleWheelDelete, then update state properly on componentDidUpdate', 148 | () => { 149 | const testWheel = { 150 | name: 'test_delete_wheel', 151 | id: shallowProps.wheelsFetch.value.Items[0].id, 152 | } 153 | const postProps = { 154 | deleteWheelFetch: { 155 | fulfilled: false, 156 | pending: false, 157 | rejected: false, 158 | } 159 | }; 160 | 161 | const wrapper = shallowWithStore( 162 | ); 163 | expect(wrapper.html()).to.contain('wheel_name_0'); 164 | wrapper.instance().handleWheelDelete(testWheel); 165 | expect(dispatchProps.dispatchDeleteWheelDelete.calledWith(testWheel.id)).to.be.true; 166 | expect(wrapper.instance().state.delete).to.be.true; 167 | 168 | wrapper.setProps({deleteWheelFetch: {fulfilled: true}}) 169 | wrapper.instance().componentDidUpdate(); 170 | expect(dispatchProps.dispatchWheelsGet.calledTwice).to.be.true; 171 | expect(wrapper.instance().state.delete).to.be.false; 172 | }); 173 | 174 | it('Should render an error message if wheels get fails', () => { 175 | const postProps = { 176 | wheelsFetch: { 177 | fulfilled: false, 178 | pending: false, 179 | rejected: true, 180 | } 181 | }; 182 | 183 | const wrapper = shallowWithStore( 184 | ); 185 | expect(wrapper.html()).to.contain('Oops... Could not fetch the wheels data!'); 186 | }); 187 | }); -------------------------------------------------------------------------------- /ui/test/globals.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | let nock = require('nock'); 17 | const { JSDOM } = require('jsdom'); 18 | import {shimData} from './shim_data'; 19 | import {createStore, applyMiddleware, combineReducers, compose} from 'redux'; 20 | import {middleware as fetchMiddleware, reducer as repository} from 'react-redux-fetch'; 21 | import * as H from 'history'; 22 | import {Provider} from 'react-redux'; 23 | import {Router} from 'react-router'; 24 | import {mount, shallow} from 'enzyme'; 25 | 26 | process.env.NODE_ENV = 'test' 27 | 28 | // Setup Enzyme 3 for react16 29 | import { configure } from 'enzyme'; 30 | import Adapter from 'enzyme-adapter-react-16'; 31 | configure({ adapter: new Adapter() }); 32 | 33 | // Deep copy the shim data 34 | let localShimData = JSON.parse(JSON.stringify(shimData)); 35 | 36 | // Setup default nocks with shim data 37 | nock('http://localhost') 38 | .persist().get('/app/api/wheel') 39 | .reply(200, { 40 | Count: localShimData.wheels.length, 41 | Items: shimData.wheels, 42 | } 43 | ); 44 | 45 | for (let wheel of localShimData.wheels) { 46 | 47 | let wheelParticipants = localShimData.participants.filter(e => e.wheel_id === wheel.id); 48 | 49 | nock('http://localhost') 50 | .persist().get(`/app/api/wheel/${wheel.id}`) 51 | .reply(200, wheel); 52 | nock('http://localhost') 53 | .persist().get(`/app/api/wheel/${wheel.id}/participant`) 54 | .reply(200, wheelParticipants); 55 | nock('http://localhost') 56 | .persist().get(`/app/api/wheel/${wheel.id}/participant/suggest`) 57 | .reply(200, { participant_id: wheelParticipants[0].id }); 58 | } 59 | 60 | nock('http://localhost') 61 | .persist().get('/app/api/config') 62 | .reply(200, {USER_POOL_ID: 'test_pool_id', APP_CLIENT_ID: 'test_client_id'} 63 | ); 64 | 65 | // Mounting wrappers 66 | export const createMockStore = () => { 67 | return createStore(combineReducers({repository}), undefined, applyMiddleware(fetchMiddleware)); 68 | }; 69 | 70 | export function mountWithRoute(component: any, router: any = { 71 | history: H.createBrowserHistory(), 72 | route: { 73 | location: H.createLocation('/'), 74 | match: { 75 | params: {}, 76 | isExact: true, 77 | path: '/', 78 | url: 'localhost/' 79 | } 80 | } 81 | }) { 82 | const childContextTypes = Object.assign(Router.childContextTypes, Provider.childContextTypes); 83 | const store = createMockStore(); 84 | const { document } = ( 85 | new JSDOM('
') 86 | ).window; 87 | return ( 88 | mount(component, { 89 | context: {store, router}, 90 | childContextTypes: childContextTypes, 91 | attachTo: document.getElementById('app') 92 | }) 93 | ); 94 | } 95 | 96 | export function shallowWithStore(component: any) { 97 | const store = createMockStore(); 98 | return shallow(component, {context: {store}}); 99 | } -------------------------------------------------------------------------------- /ui/test/index.test.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | import '../src/index'; 17 | -------------------------------------------------------------------------------- /ui/test/setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | // The DOM must be created before anything else in the test 17 | require('jsdom-global')('' + 18 | '
', {url: 'http://localhost'}); 19 | 20 | // These work around binary file import webpack functionality that breaks tests otherwise 21 | require.extensions['.mp3'] = function() { 22 | return null; 23 | }; 24 | 25 | require.extensions['.ico'] = function() { 26 | return null; 27 | }; -------------------------------------------------------------------------------- /ui/test/shim_data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | export const shimData = { 17 | 18 | wheels: [ 19 | { 20 | id: 'wheel_id_0', 21 | name: 'wheel_name_0', 22 | participant_count: 4, 23 | created_at: '2017-01-01T12:00:00.000Z', 24 | updated_at: '2017-01-01T12:00:00.000Z', 25 | }, 26 | { 27 | id: 'wheel_id_1', 28 | name: 'wheel_name_1', 29 | participant_count: 5, 30 | created_at: '2017-01-01T12:00:00.000Z', 31 | updated_at: '2017-01-01T12:00:00.000Z', 32 | }, 33 | ], 34 | participants: [ 35 | { 36 | id: 'participant_id_0', 37 | wheel_id: 'wheel_id_0', 38 | name: 'participant_name_0', 39 | url: 'http://participanturl0.url', 40 | created_at: '2017-01-01T12:00:00.000Z', 41 | updated_at: '2017-01-01T12:00:00.000Z', 42 | }, 43 | { 44 | id: 'participant_id_1', 45 | wheel_id: 'wheel_id_0', 46 | name: 'participant_name_1', 47 | url: 'http://participanturl1.url', 48 | created_at: '2017-01-01T12:00:00.000Z', 49 | updated_at: '2017-01-01T12:00:00.000Z', 50 | }, 51 | { 52 | id: 'participant_id_2', 53 | wheel_id: 'wheel_id_0', 54 | name: 'participant_name_2', 55 | url: 'http://participanturl2.url', 56 | created_at: '2017-01-01T12:00:00.000Z', 57 | updated_at: '2017-01-01T12:00:00.000Z', 58 | }, 59 | { 60 | id: 'participant_id_3', 61 | wheel_id: 'wheel_id_0', 62 | name: 'participant_name_3', 63 | url: 'http://participanturl3.url', 64 | created_at: '2017-01-01T12:00:00.000Z', 65 | updated_at: '2017-01-01T12:00:00.000Z', 66 | }, 67 | { 68 | id: 'participant_id_4', 69 | wheel_id: 'wheel_id_1', 70 | name: 'participant_name_4', 71 | url: 'http://participanturl4.url', 72 | created_at: '2017-01-01T12:00:00.000Z', 73 | updated_at: '2017-01-01T12:00:00.000Z', 74 | }, 75 | { 76 | id: 'participant_id_5', 77 | wheel_id: 'wheel_id_1', 78 | name: 'participant_name_5', 79 | url: 'http://participanturl5.url', 80 | created_at: '2017-01-01T12:00:00.000Z', 81 | updated_at: '2017-01-01T12:00:00.000Z', 82 | }, 83 | { 84 | id: 'participant_id_6', 85 | wheel_id: 'wheel_id_1', 86 | name: 'participant_name_6', 87 | url: 'http://participanturl6.url', 88 | created_at: '2017-01-01T12:00:00.000Z', 89 | updated_at: '2017-01-01T12:00:00.000Z', 90 | }, 91 | { 92 | id: 'participant_id_7', 93 | wheel_id: 'wheel_id_1', 94 | name: 'participant_name_7', 95 | url: 'http://participanturl7.url', 96 | created_at: '2017-01-01T12:00:00.000Z', 97 | updated_at: '2017-01-01T12:00:00.000Z', 98 | }, 99 | { 100 | id: 'participant_id_8', 101 | wheel_id: 'wheel_id_1', 102 | name: 'participant_name_8', 103 | url: 'http://participanturl8.url', 104 | created_at: '2017-01-01T12:00:00.000Z', 105 | updated_at: '2017-01-01T12:00:00.000Z', 106 | }, 107 | ] 108 | } -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | const webpack = require('webpack'); 16 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 17 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 18 | 19 | const ENV = process.env.NODE_ENV || 'development'; 20 | let DEV_SERVER = ''; 21 | try { 22 | DEV_SERVER = require('./development_app_location.js'); 23 | } catch(err) { 24 | // May not be initialized yet by build script, which is only an issue for running webpack server 25 | } 26 | 27 | module.exports = { 28 | cache: true, 29 | mode: 'development', 30 | devServer: { 31 | static: './', 32 | hot: true, 33 | allowedHosts: 'all', 34 | proxy: { 35 | // allows using local API endpoint when developing code 36 | '/app/api': { 37 | target: DEV_SERVER, 38 | changeOrigin: true, 39 | secure: true 40 | }, 41 | }, 42 | historyApiFallback: { 43 | rewrites: [ 44 | // allows to render index.development.html when the developer 45 | // accesses the development server using following URL 46 | // http://:, ] 168 | :type participant_details: list 169 | :param full_wheel_url: Full URL of the Wheel 170 | :type full_wheel_url: str 171 | """ 172 | participant_name = participant_details[0] 173 | participant_url = participant_details[1] 174 | headers = { 175 | 'content-type': 'application/json', 176 | 'authorization': self._authentication.id_token 177 | } 178 | payload = {'id': '', 'name': participant_name, 'url': participant_url} 179 | 180 | print('-------------------------------------------------------------') 181 | print('Uploading Participant:') 182 | print(f' - name: {participant_name}') 183 | print(f' - url: {participant_url}') 184 | 185 | try: 186 | r = requests.post( 187 | full_wheel_url, 188 | data=json.dumps(payload), 189 | headers=headers 190 | ) 191 | 192 | if r.status_code in self.STATUS_CODES_SUCCESS: 193 | print('Upload successful') 194 | else: 195 | print(f'Upload was not successful. Status Code: {r.status_code}') 196 | except Exception as e: 197 | print(f'There was an error during upload of the participant:') 198 | print(f' - name: {participant_name}') 199 | print(f' - url: {participant_url}') 200 | print(f'Following error has been raised: {e}') 201 | 202 | def _validate_csv_file(self, csv_reader): 203 | """ 204 | Performs basic format validation of the CSV file. 205 | 206 | :param csv_reader: CSV reader object 207 | :type csv_reader: obj 208 | """ 209 | for row in csv_reader: 210 | if len(row) != 2: 211 | raise CSVRowNumberOfElementsMismatch( 212 | f'Row: {row} is not valid.' 213 | ) 214 | 215 | # Rewind the reader to the beginning of the file 216 | self._csv_file.seek(0) 217 | 218 | 219 | DESCRIPTION = """ 220 | The Wheel Feeder is a script that allows you 221 | to add participants from a CSV File. 222 | 223 | You must specify either: 224 | --stack-name and --wheel-name OR --wheel-url, --wheel-id, --cognito-client-id, and --cognito-user-pool-id 225 | 226 | The format of the file is: 227 | , 228 | """ 229 | 230 | 231 | def main(): 232 | print("Initializing the Wheel Feeder...") 233 | parser = argparse.ArgumentParser( 234 | description=DESCRIPTION, 235 | formatter_class=argparse.RawTextHelpFormatter 236 | ) 237 | 238 | parser.add_argument( 239 | '-u', '--wheel-url', 240 | help='Full URL of the Wheel\'s API Gateway endpoint and stage. \n' 241 | 'Example: https://.execute-api.us-west-2.amazonaws.com/app' 242 | ) 243 | parser.add_argument( 244 | '-w', '--wheel-id', 245 | help='UUID of the Wheel which you want to feed. Alternatively you can use --wheel-name and --stack-name\n' 246 | 'Example: 57709419-17c9-4b77-ac99-77fb0d7c7c51' 247 | ) 248 | parser.add_argument( 249 | '-c', '--csv-file-path', required=True, 250 | help='Path to the CSV file. \n' 251 | 'Example: /home/foo/participants.csv' 252 | ) 253 | parser.add_argument( 254 | '-p', '--cognito-user-pool-id', 255 | help='Cognito User Pool Id. \n' 256 | 'Example: us-west-2_K4oiNOTREAL' 257 | ) 258 | parser.add_argument( 259 | '-i', '--cognito-client-id', 260 | help='Cognito Client Id (get it by visiting your Cognito User Pool). \n' 261 | 'Example: 6e6p1k4qaNOTREAL' 262 | ) 263 | parser.add_argument( 264 | '-sn', '--stack-name', 265 | help='Cloudformation stack name used during initial Wheel creation' 266 | ) 267 | parser.add_argument( 268 | '-wn', '--wheel-name', 269 | help='Wheel name. An alternative to wheel-id but requires you also specify the stack_name parameter' 270 | ) 271 | parser.add_argument( 272 | '-r', '--region', 273 | help='Region the stack is deployed in. E.G: us-east-1. ' 274 | 'Defaults to the default region in your boto/awscli configuration' 275 | ) 276 | args = parser.parse_args() 277 | if args.stack_name: 278 | cf_client = boto3.client('cloudformation', region_name=args.region) 279 | stack = cf_client.describe_stacks(StackName=args.stack_name)['Stacks'][0] 280 | stack_outputs = {output['OutputKey']: output['OutputValue'] for output in stack['Outputs']} 281 | args.cognito_client_id = args.cognito_client_id or stack_outputs['CognitoUserPoolClient'] 282 | args.cognito_user_pool_id = args.cognito_user_pool_id or stack_outputs['CognitoUserPool'] 283 | args.wheel_url = args.wheel_url or stack_outputs['Endpoint'] 284 | 285 | if args.wheel_name: 286 | ddb_client = boto3.resource('dynamodb', region_name=args.region) 287 | for item in ddb_client.Table(stack_outputs['wheelDynamoDBTable']).scan()['Items']: 288 | if item['name'] == args.wheel_name: 289 | args.wheel_id = item['id'] 290 | break 291 | else: 292 | raise SystemExit("ERROR: Could not find a wheel with the name '%s'" % args.wheel_name) 293 | 294 | if not (args.wheel_url and args.wheel_id and args.cognito_user_pool_id and args.cognito_client_id): 295 | raise SystemExit("Error: You must specify either --stack-name and --wheel-name parameters or " 296 | "--wheel-url, --wheel-id, --cognito-user-pool-id, and --cognito-client-id parameters") 297 | 298 | # Initialize the Feeder and execute it. 299 | wheel_feeder = WheelFeeder( 300 | args.wheel_url, 301 | args.wheel_id, 302 | args.csv_file_path, 303 | args.cognito_user_pool_id, 304 | args.cognito_client_id, 305 | region_name=args.region 306 | ) 307 | wheel_feeder.execute() 308 | 309 | 310 | if __name__ == "__main__": 311 | main() 312 | --------------------------------------------------------------------------------