diff --git a/.gitignore b/.gitignore index 2cba99d87..23535d79f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.idea/ +.coverage \ No newline at end of file diff --git a/clubs.json b/clubs.json index 1d7ad1ffe..cf927df86 100644 --- a/clubs.json +++ b/clubs.json @@ -1,16 +1,31 @@ -{"clubs":[ - { - "name":"Simply Lift", - "email":"john@simplylift.co", - "points":"13" - }, - { - "name":"Iron Temple", - "email": "admin@irontemple.com", - "points":"4" - }, - { "name":"She Lifts", - "email": "kate@shelifts.co.uk", - "points":"12" - } -]} \ No newline at end of file +{ + "clubs": [ + { + "name": "Simply Lift", + "email": "john@simplylift.co", + "password": "scrypt:32768:8:1$X6KRAmtt7186JPOV$d313adfa98285ed86e8b7cdb57b94e418c3cd4101aa3f268bdcbfd88e123ddb55f4c6d1383af023145d9f59bcaffcd600bbdee89e2612b0db22f8da3ac7e40c3", + "points": "13" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "password": "scrypt:32768:8:1$VmxOKXbIbWLNVJZ5$675e4bdbcd5592350046a8139e272d09c2c9a5574a76c99e6c1e6b848ed3a170c0a3fb0e18ece7ecc7d7aa8aaaa3ff8bbdc72b40cc1f92b6dee8aac76d38a822", + "points": "4" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "password": "scrypt:32768:8:1$WRC7E8hIdEXhR9bW$282910e3a7c98efd93d190c1f43b8618f334dd7332865228900ad6dde7c2487474f2a98af1247d49bbfc42a7ad92c0e69201766d403eb49eba8fc03b0d4864da", + "points": "12", + "booked_places": { + "Spring Festival": "7" + } + }, + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": "scrypt:32768:8:1$rEXX0x145nTwcwXq$7957313e13d4ca8f1b4be275af850b0c46b40225864d866cba6be6dc169dca139d34574048fa0f34fed40055672858e33c7e6b364772ce98daac750402874ae8", + "points": "5" + } + ] +} \ No newline at end of file diff --git a/competitions.json b/competitions.json index 039fc61bd..377789cb3 100644 --- a/competitions.json +++ b/competitions.json @@ -1,14 +1,24 @@ { "competitions": [ - { - "name": "Spring Festival", - "date": "2020-03-27 10:00:00", - "numberOfPlaces": "25" - }, { "name": "Fall Classic", "date": "2020-10-22 13:30:00", - "numberOfPlaces": "13" + "number_of_places": "13" + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "4" + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0" + }, + { + "name": "Spring Festival", + "date": "2026-07-27 10:00:00", + "number_of_places": "20" } ] } \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..d4c794f5b --- /dev/null +++ b/poetry.lock @@ -0,0 +1,699 @@ +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, + {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, +] + +[package.dependencies] +soupsieve = ">=1.6.1" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "os_name == \"nt\" and implementation_name != \"pypy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flask" +version = "3.1.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, + {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-testing" +version = "0.8.1" +description = "Unit testing for Flask" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Flask-Testing-0.8.1.tar.gz", hash = "sha256:0a734d7b68e63a9410b413cd7b1f96456f9a858bd09a6222d465650cc782eb01"}, +] + +[package.dependencies] +Flask = "*" + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "os_name == \"nt\" and implementation_name != \"pypy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-flask" +version = "1.3.0" +description = "A set of py.test fixtures to test Flask applications." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pytest-flask-1.3.0.tar.gz", hash = "sha256:58be1c97b21ba3c4d47e0a7691eb41007748506c36bf51004f78df10691fa95e"}, + {file = "pytest_flask-1.3.0-py3-none-any.whl", hash = "sha256:c0e36e6b0fddc3b91c4362661db83fa694d1feb91fa505475be6732b5bc8c253"}, +] + +[package.dependencies] +Flask = "*" +pytest = ">=5.2" +Werkzeug = "*" + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "selenium" +version = "4.41.0" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "selenium-4.41.0-py3-none-any.whl", hash = "sha256:b8ccde8d2e7642221ca64af184a92c19eee6accf2e27f20f30472f5efae18eb1"}, + {file = "selenium-4.41.0.tar.gz", hash = "sha256:003e971f805231ad63e671783a2b91a299355d10cefb9de964c36ff3819115aa"}, +] + +[package.dependencies] +certifi = ">=2026.1.4" +trio = ">=0.31.0,<1.0" +trio-websocket = ">=0.12.2,<1.0" +typing_extensions = ">=4.15.0,<5.0" +urllib3 = {version = ">=2.6.3,<3.0", extras = ["socks"]} +websocket-client = ">=1.8.0,<2.0" + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, + {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, +] + +[[package]] +name = "trio" +version = "0.33.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b"}, + {file = "trio-0.33.0.tar.gz", hash = "sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.12.2" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, + {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, +] + +[package.dependencies] +outcome = ">=1.2.0" +trio = ">=0.11" +wsproto = ">=0.14" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "websocket-client" +version = "1.9.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, + {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["pytest", "websockets"] + +[[package]] +name = "werkzeug" +version = "3.1.6" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"}, + {file = "werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25"}, +] + +[package.dependencies] +markupsafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wsproto" +version = "1.3.2" +description = "Pure-Python WebSocket protocol implementation" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584"}, + {file = "wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294"}, +] + +[package.dependencies] +h11 = ">=0.16.0,<1" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "9c92bf8af4fbda4326e647545db5ac22d7027cd53ce0b8215d3c9b2e6040a8e2" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..57e50dfa8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "openclassrooms-project-11" +version = "0.1.0" +description = "" +authors = [ + {name = "NM",email = "nicolas.marie.nm@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "beautifulsoup4 (>=4.14.3,<5.0.0)", + "flask (>=3.1.3,<4.0.0)", + "pytest (>=9.0.2,<10.0.0)", + "pytest-flask (>=1.3.0,<2.0.0)", + "selenium (>=4.41.0,<5.0.0)", + "flask-testing (>=0.8.1,<0.9.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/server.py b/server.py index 4084baeac..17b09b8eb 100644 --- a/server.py +++ b/server.py @@ -1,58 +1,292 @@ import json -from flask import Flask,render_template,request,redirect,flash,url_for +from datetime import datetime +from flask import Flask, render_template, request, redirect, flash, url_for, session +from werkzeug.security import generate_password_hash, check_password_hash -def loadClubs(): - with open('clubs.json') as c: - listOfClubs = json.load(c)['clubs'] - return listOfClubs +def load_clubs(): + with open('clubs.json') as c: + list_of_clubs = json.load(c)['clubs'] + return list_of_clubs -def loadCompetitions(): +def load_competitions(): with open('competitions.json') as comps: - listOfCompetitions = json.load(comps)['competitions'] - return listOfCompetitions - + list_of_competitions = json.load(comps)['competitions'] + return list_of_competitions app = Flask(__name__) app.secret_key = 'something_special' -competitions = loadCompetitions() -clubs = loadClubs() +competitions = load_competitions() +clubs = load_clubs() + +CLUB_POINTS = 15 + +def update_club_booked_places(club, places, competition_name): + clubs.remove(club) + + club.setdefault("booked_places", {}) + current = int(club["booked_places"].get(competition_name, 0)) + club["booked_places"][competition_name] = str(current + places) + + club["points"] = str(int(club["points"]) - places) + + clubs.append(club) + save_clubs() + +def save_clubs(): + with open('clubs.json', 'w') as c: + list_of_clubs = {"clubs": clubs} + json.dump(list_of_clubs, c, indent=4) + +def update_competition_available_places(competition, places): + competitions.remove(competition) + + competition['number_of_places'] = str(int(competition['number_of_places']) - places) + + competitions.append(competition) + save_competitions() + +def save_competitions(): + with open('competitions.json', 'w') as comps: + list_of_competitions = {"competitions": competitions} + json.dump(list_of_competitions, comps, indent=4) + +def add_club(name, email, password, points): + clubs.append({"name": name, "email": email, "password": password, "points": points}) + save_clubs() + +def update_club_password(club, password): + hashed_password = generate_password_hash(password) + club["password"] = hashed_password + save_clubs() + return club @app.route('/') def index(): return render_template('index.html') -@app.route('/showSummary',methods=['POST']) -def showSummary(): - club = [club for club in clubs if club['email'] == request.form['email']][0] - return render_template('welcome.html',club=club,competitions=competitions) +@app.route('/signUp') +def sign_up(): + return render_template('sign_up.html') +@app.route('/profile/', methods=['GET']) +def profile(club): + if "club" in session and session['club'] == club: + the_club = next((c for c in clubs if c['name'] == club), None) + + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Club not found"), 404 + + return render_template(template_name_or_list='profile.html', club=the_club) + + flash("Sorry, you are not allow to see that profile.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + +@app.route('/profile', methods=['POST']) +def profile_post(): + club_name = request.form['name'] + club_email = request.form['email'] + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + club_exists = next((c for c in clubs if c['email'] == club_email or c['name'] == club_name), None) + if club_exists is None: + if club_password != club_password_confirmation: + flash('Sorry, passwords do not match') + return redirect(url_for('sign_up')) + + hashed_password = generate_password_hash(club_password) + add_club(club_name, club_email, hashed_password, str(CLUB_POINTS)) + + the_club = next((c for c in clubs if c['email'] == club_email), None) + + if the_club is None: + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='sign_up.html') + + flash("Great! You have successfully signed up.") + return render_template(template_name_or_list='profile.html', club=the_club) -@app.route('/book//') -def book(competition,club): - foundClub = [c for c in clubs if c['name'] == club][0] - foundCompetition = [c for c in competitions if c['name'] == competition][0] - if foundClub and foundCompetition: - return render_template('booking.html',club=foundClub,competition=foundCompetition) else: - flash("Something went wrong-please try again") - return render_template('welcome.html', club=club, competitions=competitions) + flash("Sorry, the club already exists.") + return render_template(template_name_or_list='sign_up.html') + +@app.route('/changePassword/', methods=['GET', 'POST']) +def change_password(club): + if "club" in session and session['club'] == club: + if request.method == 'GET': + the_club = next((c for c in clubs if c['name'] == club), None) + + if the_club is None: + flash("Sorry, that club was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + return render_template(template_name_or_list='change_password.html', club=the_club) + else: + club_password = request.form['password'] + club_password_confirmation = request.form['confirm_password'] + + if club_password != club_password_confirmation: + flash('Sorry, passwords do not match') + return redirect(url_for('change_password')) + + the_club = next((c for c in clubs if c['name'] == club), None) + + if check_password_hash(the_club['password'], club_password): + flash('Sorry, you have to type a new different password.') + return render_template(template_name_or_list='change_password.html', club=the_club) + + the_club = update_club_password(the_club, club_password) + if the_club: + flash("Great! You have successfully changed your password.") + return render_template(template_name_or_list='profile.html', club=the_club) -@app.route('/purchasePlaces',methods=['POST']) -def purchasePlaces(): + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='index.html') + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + + +@app.route('/showSummary/', methods=['GET']) +def show_summary(club): + if "club" in session and session['club'] == club: + the_club = next((c for c in clubs if c['name'] == club), None) + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions) + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + +@app.route('/showSummary', methods=['POST']) +def show_summary_post(): + the_club = next((c for c in clubs if c['email'] == request.form['email']), None) + + if the_club is None: + flash("Sorry, that email was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + if not check_password_hash(the_club['password'], request.form['password']): + flash("Sorry, the password is incorrect.") + return render_template(template_name_or_list="index.html",) + + session["club"] = the_club["name"] + + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions) + +@app.route('/book//') +def book(competition, club): + if "club" in session and session['club'] == club: + found_club = [c for c in clubs if c['name'] == club][0] + found_competition = [c for c in competitions if c['name'] == competition][0] + + now = datetime.now() + + competition_date = datetime.strptime(found_competition['date'], '%Y-%m-%d %H:%M:%S') + + error_message = "" + error_tag = "" + + the_competition = next((a_competition for a_competition in competitions + if a_competition['name'] == competition), None) + competition_places = int(the_competition['number_of_places']) + + if now > competition_date: + error_message = "Sorry, this competition is outdated. Booking not possible." + error_tag = "Outdated" + + elif competition_places == 0: + error_message = "Sorry, this competition is sold out. Booking not possible." + error_tag = "Sold out" + + if error_message and error_tag: + flash(error_message) + + the_club = next((a_club for a_club in clubs if a_club['name'] == club), None) + + return render_template(template_name_or_list='welcome.html', + club=the_club, + competitions=competitions, + error=error_tag), 403 + + if found_club and found_competition: + return render_template(template_name_or_list='booking.html', + club=found_club, + competition=found_competition) + else: + flash("Sorry, something went wrong. Please try again.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) + + flash("Sorry, you are not allow to do this action.") + return render_template(template_name_or_list='index.html', error="Not allow"), 403 + +@app.route('/purchasePlaces', methods=['POST']) +def purchase_places(): competition = [c for c in competitions if c['name'] == request.form['competition']][0] club = [c for c in clubs if c['name'] == request.form['club']][0] - placesRequired = int(request.form['places']) - competition['numberOfPlaces'] = int(competition['numberOfPlaces'])-placesRequired - flash('Great-booking complete!') - return render_template('welcome.html', club=club, competitions=competitions) + places_required = int(request.form['places']) + + cumulative_places = places_required + int(club["booked_places"][competition["name"]]) \ + if "booked_places" in club else places_required + + error_message = "" + error_tag = "" + + if places_required < 0: + error_message = "Sorry, you should type a positive number." + error_tag = "Negative number" + + elif cumulative_places > 12: + error_message = "Sorry, you are not allow to purchase more than 12 places for this competition." + error_tag = "Over 12 places" + + elif places_required > int(competition['number_of_places']): + error_message = "Sorry, there are not enough places available for this competition." + error_tag = "Not enough places" + + elif places_required > int(club['points']): + error_message = "Sorry, you do not have enough points to purchase." + error_tag = "Not enough points" + + if error_message and error_tag: + flash(error_message) + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error=error_tag), 403 + + update_club_booked_places(club=club, + places=places_required, + competition_name=competition["name"]) + + update_competition_available_places(competition=competition, places=places_required) + flash(f"Great! Booking of {places_required} places for " + f"{competition['name']} competition complete!") -# TODO: Add route for points display + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) +@app.route('/pointsBoard') +def points_board(): + clubs_for_board=[] + for club in clubs: + if clubs.index(club) %2 == 0: + club["color"] = "#cccccc" + else: + club["color"] = "#aaaaaa" + clubs_for_board.append(club) + return render_template(template_name_or_list='points_board.html', clubs=clubs_for_board) @app.route('/logout') def logout(): diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 000000000..cb24cfe32 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,37 @@ +.board{ + margin-left: 50px; + display: block; + border:1px black solid; + border-radius: 10px; + width: fit-content; +} + +.board table{ + width:400px; +} + +.board table tr{ + width: 100%; +} + +.board table thead div{ + background-color: #000000; + color: white; + font-weight: bold; +} + +.board table td{ + color: black; +} + +.board table td p{ + text-align: center; +} + +.board .cell{ + border-radius: 10px; + align-items: center; + display: flex; + justify-content: center; + padding: 5px 10px; +} \ No newline at end of file diff --git a/templates/booking.html b/templates/booking.html index 06ae1156c..a9b12c600 100644 --- a/templates/booking.html +++ b/templates/booking.html @@ -6,7 +6,7 @@

{{competition['name']}}

- Places available: {{competition['numberOfPlaces']}} + Places available: {{competition['number_of_places']}}
diff --git a/templates/change_password.html b/templates/change_password.html new file mode 100644 index 000000000..7fbd77c33 --- /dev/null +++ b/templates/change_password.html @@ -0,0 +1,35 @@ + + + + + GUDLFT Registration + + +

Welcome to the GUDLFT - Change password page !

+ + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {%endwith%} + + Please enter your new password:

+ + Name : {{club['name']}}
+ Email : {{club['email']}} +

+ + + + + + + + +
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 926526b7d..9c463cd23 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,11 +6,31 @@

Welcome to the GUDLFT Registration Portal!

- Please enter your secretary email to continue: + + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {%endwith%} + + Please enter your secretary email and your password to continue or sign up +
+
- - + + + + + +
+
+
+ To see the clubs points board : click here \ No newline at end of file diff --git a/templates/points_board.html b/templates/points_board.html new file mode 100644 index 000000000..e3f4df107 --- /dev/null +++ b/templates/points_board.html @@ -0,0 +1,31 @@ + + + + + GUDLFT Registration + + + +

Welcome to the GUDLFT clubs points board!

+
+

⯈ Here is the board for all the clubs and their points.

+
+ + + + + + {% for club in clubs %} + + + + + {% endfor %} +
Club
Points
{{ club.name }}
{{ club.points }}
+
+
+
+ To continue into the application, please log in or sign up +
+ + \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 000000000..9f9484cdb --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,32 @@ + + + + + Summary | GUDLFT Profile + + +

Welcome, {{club['email']}}

- Logout + + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} +

Profile:

+
    +
  • Name : {{club['name']}}
  • +
  • Email : {{club['email']}}
  • +
  • password : {{club['password']}} - Change password
  • +
    +
  • Points available: {{club['points']}}
  • +
+
+ Go to home page + + {%endwith%} + + + \ No newline at end of file diff --git a/templates/sign_up.html b/templates/sign_up.html new file mode 100644 index 000000000..9c16dd540 --- /dev/null +++ b/templates/sign_up.html @@ -0,0 +1,37 @@ + + + + + GUDLFT Sign up + + +

Welcome to the GUDLFT sign up page!

+ + {% with messages = get_flashed_messages()%} + {% if messages %} +
    + {% for message in messages %} +
  • {{message}}
  • + {% endfor %} +
+ {% endif%} + {%endwith%} + + Please enter your secretary email and your password: +
+ + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/templates/welcome.html b/templates/welcome.html index ff6b261a2..355e23b3d 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -5,7 +5,7 @@ Summary | GUDLFT Registration -

Welcome, {{club['email']}}

Logout +

Welcome, {{club['email']}}

Logout

{% with messages = get_flashed_messages()%} {% if messages %} @@ -15,6 +15,9 @@

Welcome, {{club['email']}}

Logout {% endfor %} {% endif%} + + Go to profile +
Points available: {{club['points']}}

Competitions:

    @@ -22,8 +25,8 @@

    Competitions:

  • {{comp['name']}}
    Date: {{comp['date']}}
    - Number of Places: {{comp['numberOfPlaces']}} - {%if comp['numberOfPlaces']|int >0%} + Number of Places: {{comp['number_of_places']}} + {%if comp['number_of_places']|int >0%} Book Places {%endif%}
  • diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..11cc47d0c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,194 @@ +import threading +import pytest +import time + +from random import randint +from werkzeug.security import generate_password_hash +from server import app + +@pytest.fixture +def client(): + my_app = app + with my_app.test_client() as client: + yield client + +@pytest.fixture +def get_clubs(): + the_clubs = [ + { + "name":"Simply Lift", + "email":"john@simplylift.co", + "password": generate_password_hash("tp1_Tmn28"), + "points":"13" + }, + { + "name":"Iron Temple", + "email": "admin@irontemple.com", + "password": generate_password_hash("tp2_Tmn29"), + "points":"4" + }, + { "name":"She Lifts", + "email": "kate@shelifts.co.uk", + "password": generate_password_hash("tp3_Tmn30"), + "points":"12", + "booked_places": { + "Spring Festival": "7" + } + }, + { + "name": "Power Lift", + "email": "admin@powerlift.com", + "password": generate_password_hash("tp4_Tmn40"), + "points": "5" + } + ] + return the_clubs + +@pytest.fixture +def get_competitions(): + the_competitions = [ + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "number_of_places": "13" + }, + { + "name": "Spring Festival", + "date": "2026-07-27 10:00:00", + "number_of_places": "25" + }, + { + "name": "Winter Power", + "date": "2026-06-26 12:16:00", + "number_of_places": "4" + }, + { + "name": "Summer Stronger", + "date": "2026-05-30 18:23:40", + "number_of_places": "0" + } + ] + return the_competitions + +@pytest.fixture +def get_credentials(): + data = {"email": "kate@shelifts.co.uk", "password": "tp3_Tmn30"} + return data + +@pytest.fixture +def get_credentials_2(): + data = {"email": "admin@irontemple.com", "password": "tp2_Tmn29"} + return data + +@pytest.fixture +def get_credentials_3(): + data = {"email": "admin@powerlift.com", "password": "tp4_Tmn40"} + return data + +@pytest.fixture +def get_unexisting_credentials(): + data = {"email": "nicolas.marie@unexisting.com", "password": "er45_shet"} + return data + +@pytest.fixture +def get_existing_competition_and_club(): + data = {"competition": "Spring Festival", "club": "She Lifts"} + return data + +@pytest.fixture +def get_existing_competition_and_club_2(): + data = {"competition": "Spring Festival", "club": "Iron Temple"} + return data + +@pytest.fixture +def get_existing_competition_and_club_3(): + data = {"competition": "Fall Classic", "club": "Iron Temple"} + return data + +@pytest.fixture +def get_existing_competition_and_club_4(): + data = {"competition": "Summer Stronger", "club": "Power Lift"} + return data + +@pytest.fixture +def get_consistent_purchasing_data(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = randint(1, 5) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def get_new_club(): + club = { + "name": "New Club", + "email": "new@newclub.com", + "password": generate_password_hash("tp6_Tmn60"), + "points": "12" + } + return club + +@pytest.fixture +def get_inconsistent_purchasing_data(): + competition = "Spring Festival" + club_name = "Iron Temple" + club_points = 4 + places_to_book = randint(club_points+1, 12) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_over_12_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 13 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_13_cumulative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = 6 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_with_negative_places(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = -2 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture +def purchasing_places_more_than_available(): + competition = "Winter Power" + club_name = "Power Lift" + places_to_book = 5 + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@pytest.fixture(scope="module") +def live_server(): + app.config["TESTING"] = True + + ctx = app.app_context() + ctx.push() + + server_thread = threading.Thread( + target=app.run, + kwargs={"port": 5000, "use_reloader": False}, + daemon=True + ) + server_thread.start() + time.sleep(1) + + yield "http://127.0.0.1:5000" + ctx.pop() diff --git a/tests/functional_tests/__init__.py b/tests/functional_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional_tests/geckodriver.exe b/tests/functional_tests/geckodriver.exe new file mode 100644 index 000000000..59b6a49eb Binary files /dev/null and b/tests/functional_tests/geckodriver.exe differ diff --git a/tests/functional_tests/test_app.py b/tests/functional_tests/test_app.py new file mode 100644 index 000000000..cc30dcc9c --- /dev/null +++ b/tests/functional_tests/test_app.py @@ -0,0 +1,59 @@ +import pytest +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.firefox.service import Service + + +class TestFunctionalApp: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') + + def test_signup(self, live_server): + options = Options() + options.binary_location = r"C:\Program Files\Mozilla Firefox\firefox.exe" + + gecko_service = Service(executable_path="tests/functional_tests/geckodriver.exe") + + browser = webdriver.Firefox(service=gecko_service, options=options) + browser.get(f"{live_server}/signUp") + + name = browser.find_element(By.ID, "name") + name.send_keys("Test Club") + email = browser.find_element(By.ID, "email") + email.send_keys("doe@testclub.com") + password1 = browser.find_element(By.ID, "password") + password1.send_keys("tgl_Prn_C6") + password2 = browser.find_element(By.ID, "confirm-password") + password2.send_keys("tgl_Prn_C6") + signup = browser.find_element(By.ID, "signup") + signup.click() + + assert browser.find_element("tag name", "h2").text == "Welcome, doe@testclub.com" + + browser.quit() + + def test_login(self, live_server, get_credentials): + options = Options() + options.binary_location = r"C:\Program Files\Mozilla Firefox\firefox.exe" + + gecko_service = Service(executable_path="tests/functional_tests/geckodriver.exe") + + browser = webdriver.Firefox(service=gecko_service, options=options) + browser.get(f"{live_server}/") + + email = browser.find_element(By.ID, "email") + email.send_keys(get_credentials["email"]) + password = browser.find_element(By.ID, "password") + password.send_keys(get_credentials["password"]) + login = browser.find_element(By.ID, "login") + login.click() + + assert browser.find_element("tag name", "h2").text == "Welcome, kate@shelifts.co.uk" + + browser.quit() diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration_tests/test_app.py b/tests/integration_tests/test_app.py new file mode 100644 index 000000000..f1787ab42 --- /dev/null +++ b/tests/integration_tests/test_app.py @@ -0,0 +1,241 @@ +import pytest +import server + +from bs4 import BeautifulSoup +from flask import url_for + + +class TestIntegrationApp: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') + + @staticmethod + def test_summary_logout_redirect_returns_welcome(client, get_credentials): + client.post('/showSummary', data=get_credentials) + + logout_response = client.get('/logout') + soup = BeautifulSoup(logout_response.data.decode(), features="html.parser") + url = soup.find_all('a')[0].get('href') + redirect_response = client.get(url, follow_redirects=True) + + assert redirect_response.status_code == 200 + data = redirect_response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert ('Please enter your secretary email and your password to continue ' + 'or sign up') in data + assert "Email:" in data + assert "Password:" in data + + @staticmethod + def test_booking_return_festival_page_booking(client, + get_credentials, + get_existing_competition_and_club): + + client.post('/showSummary', data=get_credentials) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + data = client_response.data.decode('utf-8') + assert "Spring Festival" in data + assert "Places available: " in data + assert "How many places?" in data + + @staticmethod + def test_good_purchasing_places_returns_summary_page(client, + get_credentials, + get_consistent_purchasing_data): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = get_consistent_purchasing_data + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + the_competition =[competition for competition in server.competitions + if competition["name"] == purchasing_data['competition']][0] + + client.get(url_for(endpoint='book', + competition=the_competition['name'], + club=the_club['name'])) + + club_points = the_club['points'] + competition_places = the_competition['number_of_places'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + new_points = int(club_points) - int(purchasing_data['places']) + new_competition_places = int(competition_places) - int(purchasing_data['places']) + + soup = BeautifulSoup(data, features="html.parser") + all_li_str = [str(li) for li in soup.find_all('li')] + the_club_name_utf8 = "%20".join(the_club['name'].split()) + the_competition_name_utf8 = "%20".join(the_competition['name'].split()) + li = (f'
  • \n' + f' {the_competition["name"]}
    \n' + f' Date: 2026-07-27 10:00:00\n' + f' Number of Places: {new_competition_places}\n \n' + f' Book Places\n' + f'
  • ') + + assert client_response.status_code == 200 + assert (f"Great! Booking of {purchasing_data['places']} places for " + f"{purchasing_data['competition']} competition complete!") in data + assert f"Welcome, {the_club["email"]} " in data + assert li in all_li_str + assert f"Points available: {new_points}" in data + + @staticmethod + def test_purchasing_places_not_enough_points_returns_sorry(client, + get_credentials_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data): + + client.post('/showSummary', data=get_credentials_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) + + the_club = [club for club in server.clubs + if club["name"] == get_existing_competition_and_club_2['club']][0] + + client_response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you do not have enough points to purchase." in data + assert f"Points available: {the_club['points']}" in data + + @staticmethod + def test_purchasing_places_over_12_places_returns_sorry(client, + get_credentials, + purchasing_over_12_places): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_over_12_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_over_12_cumulative_places_returns_sorry(client, + get_credentials, + purchasing_13_cumulative_places): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_13_cumulative_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you are not allow to purchase more than 12 places for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_negative_number_returns_sorry(client, + get_credentials, + purchasing_with_negative_places): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_with_negative_places + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, you should type a positive number." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_past_competitions_returns_sorry(client, + get_credentials_2, + get_existing_competition_and_club_3): + + client.post('/showSummary', data=get_credentials_2) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_3['competition'], + club=get_existing_competition_and_club_3['club'])) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert "Sorry, this competition is outdated. Booking not possible." in data + + @staticmethod + def test_purchasing_places_over_available_returns_sorry(client, + get_credentials, + purchasing_places_more_than_available): + + client.post('/showSummary', data=get_credentials) + + purchasing_data = purchasing_places_more_than_available + + client.get(url_for(endpoint='book', + competition=purchasing_data['competition'], + club=purchasing_data['club'])) + + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + client_response = client.post('/purchasePlaces', data=purchasing_data) + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert f"Welcome, {the_club["email"]} " in data + assert "Sorry, there are not enough places available for this competition." in data + assert f"Points available: {club_points}" in data + + @staticmethod + def test_purchasing_places_sold_out_status_code_error(client, + get_credentials_3, + get_existing_competition_and_club_4): + + client.post('/showSummary', data=get_credentials_3) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_4['competition'], + club=get_existing_competition_and_club_4['club'])) + + data = client_response.data.decode('utf-8') + + assert client_response.status_code == 403 + assert "Sorry, this competition is sold out. Booking not possible." in data diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py new file mode 100644 index 000000000..daa3cb6bc --- /dev/null +++ b/tests/unit_tests/test_app.py @@ -0,0 +1,190 @@ +import pytest +import server + +from werkzeug.security import check_password_hash +from flask import url_for + + +class TestUnitApp: + @pytest.fixture(autouse=True) + def setup(self, mocker, get_clubs, get_competitions): + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + mocker.patch('server.save_clubs') + + @staticmethod + def test_index_status_code_ok(client): + client_response = client.get('/') + assert client_response.status_code == 200 + + @staticmethod + def test_index_return_welcome(client): + client_response = client.get('/') + data = client_response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert ('Please enter your secretary email and your password to continue ' + 'or sign up') in data + assert "Email:" in data + assert "Password:" in data + + @staticmethod + def test_index_mail_authentication_ok(get_credentials, client): + client_response = client.post('/showSummary', data=get_credentials) + assert client_response.status_code == 200 + + @staticmethod + def test_index_mail_authentication_returns_summary(client, get_credentials): + client_response = client.post('/showSummary', data=get_credentials) + data = client_response.data.decode('utf-8') + + assert "Welcome, kate@shelifts.co.uk" in data + assert "Spring Festival" in data + assert "Fall Classic" in data + assert "Points available: 12" in data + + @staticmethod + def test_index_mail_authentication_fail(client, get_unexisting_credentials): + client_response = client.post('/showSummary', data=get_unexisting_credentials) + data = client_response.data.decode('utf-8') + assert client_response.status_code == 404 + assert "Sorry, that email was not found." in data + + @staticmethod + def test_summary_logout_redirect_status_code_ok(client, get_credentials): + client.post('/showSummary', data=get_credentials) + logout_response = client.get('/logout') + assert logout_response.status_code == 302 + + @staticmethod + def test_booking_status_code_ok(client, + get_credentials, + get_existing_competition_and_club): + + client.post('/showSummary', data=get_credentials) + + client_response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + assert client_response.status_code == 200 + + @staticmethod + def test_signup_status_code_ok(client): + client_response = client.get('/signUp') + assert client_response.status_code == 200 + + @staticmethod + def test_signup_returns_welcome(client): + client_response = client.get('/signUp') + data = client_response.data.decode('utf-8') + assert "Welcome to the GUDLFT sign up page!" in data + assert ("Club name:" in data) + assert ("Email:" in data) + assert ("Password:" in data) + assert ("Confirm Password:" in data) + + @staticmethod + def test_change_password_status_code_ok(client): + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/changePassword/She Lifts') + assert client_response.status_code == 200 + + @staticmethod + def test_update_password_ok(get_credentials): + the_club = next((c for c in server.clubs if c['email'] == get_credentials["email"]), None) + the_club = server.update_club_password(the_club, "tp5_Tmn50") + + assert check_password_hash(the_club['password'], "tp5_Tmn50") + + @staticmethod + def test_update_password_fails(get_credentials): + the_club = next((c for c in server.clubs if c['email'] == get_credentials["email"]), None) + the_club = server.update_club_password(the_club, "tp5_Tmn50") + + assert not check_password_hash(the_club['password'], "tp6_Tmn60") + + @staticmethod + def test_add_club_ok(get_new_club): + server.add_club(name=get_new_club["name"], + email=get_new_club["email"], + password=get_new_club["password"], + points=get_new_club["points"]) + + assert get_new_club in server.clubs + + @staticmethod + def test_profile_ok(client): + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') + assert client_response.status_code == 200 + + @staticmethod + def test_profile_without_authentication_fails(client): + client_response = client.get('/profile/Simply Lift') + assert client_response.status_code == 403 + + @staticmethod + def test_profile_returns_welcome(client): + with client.session_transaction() as session: + session["club"] = "She Lifts" + + client_response = client.get('/profile/She Lifts') + data = client_response.data.decode('utf-8') + assert "Welcome, kate@shelifts.co.uk" in data + assert "Profile:" in data + assert "Name : She Lifts" in data + assert "Email : kate@shelifts.co.uk" in data + assert "Points available: 12" in data + + @staticmethod + def test_update_booked_places_ok(get_existing_competition_and_club_2): + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in server.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + server.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert club['booked_places'][competition_name] == str(5) + + @staticmethod + def test_update_booked_places_fails(get_existing_competition_and_club_2): + club_name = get_existing_competition_and_club_2['club'] + club = next((c for c in server.clubs if c['name'] == club_name), None) + competition_name = get_existing_competition_and_club_2['competition'] + server.update_club_booked_places(club=club, + places=5, + competition_name=competition_name) + + assert not club['booked_places'][competition_name] == str(6) + + @staticmethod + def test_update_competition_available_places(get_existing_competition_and_club): + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in server.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + server.update_competition_available_places(competition=competition, places=5) + + assert competition['number_of_places'] == str(places_available - 5) + + @staticmethod + def test_update_competition_available_places_fails(get_existing_competition_and_club): + competition_name = get_existing_competition_and_club['competition'] + competition = next((c for c in server.competitions if c['name'] == competition_name), None) + places_available = int(competition['number_of_places']) + server.update_competition_available_places(competition=competition, places=5) + + assert not competition['number_of_places'] == str(places_available - 4) + + @staticmethod + def test_points_board_status_code_ok(client): + client_response = client.get('/pointsBoard') + data = client_response.data.decode('utf-8') + assert client_response.status_code == 200 + assert "Welcome to the GUDLFT clubs points board!" in data + assert "⯈ Here is the board for all the clubs and their points." in data