From 940312f977e705bb436be614e63074072b54ad77 Mon Sep 17 00:00:00 2001
From: Felix Chadwick-Smith <chadwick-smithff@cardiff.ac.uk>
Date: Wed, 10 Jan 2024 19:39:57 +0000
Subject: [PATCH] changed location to site.db and added Flask_WTF

---
 my_flask_app.py                               |  38 +-
 .../flask_wtf-1.2.1.dist-info/INSTALLER       |   1 +
 .../flask_wtf-1.2.1.dist-info/METADATA        |  72 ++++
 .../flask_wtf-1.2.1.dist-info/RECORD          |  26 ++
 .../flask_wtf-1.2.1.dist-info/REQUESTED       |   0
 .../flask_wtf-1.2.1.dist-info/WHEEL           |   4 +
 .../licenses/LICENSE.rst                      |  28 ++
 venv/Lib/site-packages/flask_wtf/__init__.py  |   8 +
 .../__pycache__/__init__.cpython-311.pyc      | Bin 0 -> 532 bytes
 .../__pycache__/_compat.cpython-311.pyc       | Bin 0 -> 698 bytes
 .../__pycache__/csrf.cpython-311.pyc          | Bin 0 -> 15040 bytes
 .../__pycache__/file.cpython-311.pyc          | Bin 0 -> 8377 bytes
 .../__pycache__/form.cpython-311.pyc          | Bin 0 -> 7261 bytes
 .../__pycache__/i18n.cpython-311.pyc          | Bin 0 -> 2060 bytes
 venv/Lib/site-packages/flask_wtf/_compat.py   |  11 +
 venv/Lib/site-packages/flask_wtf/csrf.py      | 329 ++++++++++++++++++
 venv/Lib/site-packages/flask_wtf/file.py      | 147 ++++++++
 venv/Lib/site-packages/flask_wtf/form.py      | 127 +++++++
 venv/Lib/site-packages/flask_wtf/i18n.py      |  47 +++
 .../flask_wtf/recaptcha/__init__.py           |   3 +
 .../__pycache__/__init__.cpython-311.pyc      | Bin 0 -> 380 bytes
 .../__pycache__/fields.cpython-311.pyc        | Bin 0 -> 1134 bytes
 .../__pycache__/validators.cpython-311.pyc    | Bin 0 -> 3739 bytes
 .../__pycache__/widgets.cpython-311.pyc       | Bin 0 -> 2875 bytes
 .../flask_wtf/recaptcha/fields.py             |  17 +
 .../flask_wtf/recaptcha/validators.py         |  75 ++++
 .../flask_wtf/recaptcha/widgets.py            |  43 +++
 27 files changed, 958 insertions(+), 18 deletions(-)
 create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER
 create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA
 create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD
 create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED
 create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL
 create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst
 create mode 100644 venv/Lib/site-packages/flask_wtf/__init__.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/_compat.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/csrf.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/file.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/form.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/i18n.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/fields.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/validators.py
 create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py

diff --git a/my_flask_app.py b/my_flask_app.py
index efe9112..25ce352 100644
--- a/my_flask_app.py
+++ b/my_flask_app.py
@@ -2,7 +2,9 @@ import os
 import secrets
 from flask import Flask, render_template, request, redirect, url_for, send_from_directory, abort
 from flask_sqlalchemy import SQLAlchemy
-from models import db, Project
+from flask_wtf import FlaskForm
+from wtforms import StringField, TextAreaField, SubmitField
+from wtforms.validators import DataRequired
 
 app = Flask(__name__, static_folder='static')
 app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
@@ -10,11 +12,17 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.abspath(os.path.j
 app.config['SECRET_KEY'] = secrets.token_hex(16)
 
 db = SQLAlchemy(app)
+
 class Project(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     title = db.Column(db.String(100), nullable=False)
     description = db.Column(db.Text, nullable=False)
 
+class ProjectForm(FlaskForm):
+    title = StringField('Title', validators=[DataRequired()])
+    description = TextAreaField('Description', validators=[DataRequired()])
+    submit = SubmitField('Submit')
+
 @app.route('/')
 def home():
     try:
@@ -46,23 +54,20 @@ def portfolio():
 def contact():
     return render_template('contact.html')
 
-# Updated route for adding a project without Flask-WTF form
+# Updated route for adding a project with Flask-WTF form
 @app.route('/add_project', methods=['GET', 'POST'])
 def add_project():
-    if request.method == 'POST':
-        # Retrieve form data directly from request
-        title = request.form.get('title')
-        description = request.form.get('description')
-
-        # Print or log the form data to check if it's received
-        print(f"Received form data - Title: {title}, Description: {description}")
+    form = ProjectForm()
+    if form.validate_on_submit():
+        title = form.title.data
+        description = form.description.data
 
         new_project = Project(title=title, description=description)
         db.session.add(new_project)
         db.session.commit()
         return redirect(url_for('home'))
 
-    return render_template('add_project.html')
+    return render_template('add_project.html', form=form)
 
 # Updated route for serving the 'my-cv.docx' file
 @app.route('/download_cv')
@@ -75,20 +80,17 @@ def download_cv():
 @app.route('/download_assessment/<filename>')
 def download_assessment(filename):
     try:
-        file_path = f'static/{filename}'
-        print(f"Attempting to serve file: {file_path}")
+        file_path = os.path.join('static', filename)
+        app.logger.debug(f"Attempting to serve file: {file_path}")
         return send_from_directory('static', filename, as_attachment=True)
     except FileNotFoundError:
-        print(f"File not found: {file_path}")
+        app.logger.error(f"File not found: {file_path}")
         abort(404)  # Return a 404 Not Found error
     except Exception as e:
-        print(f"Error serving assessment file: {str(e)}")
-        app.logger.exception(f"Error serving assessment file: {str(e)}")
-        abort(500) 
+        app.logger.error(f"Error serving assessment file: {str(e)}")
+        abort(500)
 
 if __name__ == '__main__':
     with app.app_context():
         db.create_all()
     app.run(debug=True, port=int(os.environ.get('PORT', 8080)))
-
-
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA
new file mode 100644
index 0000000..92f1ff2
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA
@@ -0,0 +1,72 @@
+Metadata-Version: 2.1
+Name: Flask-WTF
+Version: 1.2.1
+Summary: Form rendering, validation, and CSRF protection for Flask with WTForms.
+Project-URL: Documentation, https://flask-wtf.readthedocs.io/
+Project-URL: Changes, https://flask-wtf.readthedocs.io/changes/
+Project-URL: Source Code, https://github.com/wtforms/flask-wtf/
+Project-URL: Issue Tracker, https://github.com/wtforms/flask-wtf/issues/
+Project-URL: Chat, https://discord.gg/pallets
+Maintainer: WTForms
+License: Copyright 2010 WTForms
+        
+        Redistribution and use in source and binary forms, with or without
+        modification, are permitted provided that the following conditions are
+        met:
+        
+        1.  Redistributions of source code must retain the above copyright
+            notice, this list of conditions and the following disclaimer.
+        
+        2.  Redistributions in binary form must reproduce the above copyright
+            notice, this list of conditions and the following disclaimer in the
+            documentation and/or other materials provided with the distribution.
+        
+        3.  Neither the name of the copyright holder nor the names of its
+            contributors may be used to endorse or promote products derived from
+            this software without specific prior written permission.
+        
+        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+        "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+        LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+        PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+        HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+        SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+        TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+        PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+        LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+        NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+        SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+License-File: LICENSE.rst
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
+Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Requires-Python: >=3.8
+Requires-Dist: flask
+Requires-Dist: itsdangerous
+Requires-Dist: wtforms
+Provides-Extra: email
+Requires-Dist: email-validator; extra == 'email'
+Description-Content-Type: text/x-rst
+
+Flask-WTF
+=========
+
+Simple integration of Flask and WTForms, including CSRF, file upload,
+and reCAPTCHA.
+
+Links
+-----
+
+-   Documentation: https://flask-wtf.readthedocs.io/
+-   Changes: https://flask-wtf.readthedocs.io/changes/
+-   PyPI Releases: https://pypi.org/project/Flask-WTF/
+-   Source Code: https://github.com/wtforms/flask-wtf/
+-   Issue Tracker: https://github.com/wtforms/flask-wtf/issues/
+-   Chat: https://discord.gg/pallets
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD
new file mode 100644
index 0000000..a781466
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD
@@ -0,0 +1,26 @@
+flask_wtf-1.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+flask_wtf-1.2.1.dist-info/METADATA,sha256=9Y5upDJ7WU2m2l4erWImF3HcVSWIZKH3TdX6klYpq4M,3373
+flask_wtf-1.2.1.dist-info/RECORD,,
+flask_wtf-1.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+flask_wtf-1.2.1.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
+flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst,sha256=1fGQNkUVeMs27u8EyZ6_fXyi5w3PBDY2UZvEIOFafGI,1475
+flask_wtf/__init__.py,sha256=x6ydw5SJzsXZgz-Y6IM_95Sy8VufRepvZH1DUIlFoTo,214
+flask_wtf/__pycache__/__init__.cpython-311.pyc,,
+flask_wtf/__pycache__/_compat.cpython-311.pyc,,
+flask_wtf/__pycache__/csrf.cpython-311.pyc,,
+flask_wtf/__pycache__/file.cpython-311.pyc,,
+flask_wtf/__pycache__/form.cpython-311.pyc,,
+flask_wtf/__pycache__/i18n.cpython-311.pyc,,
+flask_wtf/_compat.py,sha256=N3sqC9yzFWY-3MZ7QazX1sidvkO3d5yy4NR6lkp0s94,248
+flask_wtf/csrf.py,sha256=O-fjnWygxxi_FsIU2koua97ZpIhiOJVDHA57dXLpvTA,10171
+flask_wtf/file.py,sha256=AsfkYTCgtqGWySimc_NjeAxg-DtpdcthhqMLrXIDAhU,4706
+flask_wtf/form.py,sha256=TmR7xCrxin2LHp6thn7fq1OeU8aLB7xsZzvv52nH7Ss,4049
+flask_wtf/i18n.py,sha256=TyO8gqt9DocHMSaNhj0KKgxoUrPYs-G1nVW-jns0SOw,1166
+flask_wtf/recaptcha/__init__.py,sha256=m4eNGoU3Q0Wnt_wP8VvOlA0mwWuoMtAcK9pYT7sPFp8,106
+flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc,,
+flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc,,
+flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc,,
+flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc,,
+flask_wtf/recaptcha/fields.py,sha256=M1-RFuUKOsJAzsLm3xaaxuhX2bB9oRqS-HVSN-NpkmI,433
+flask_wtf/recaptcha/validators.py,sha256=3sd1mUQT3Y3D_WJeKwecxUGstnhh_QD-A_dEBJfkf6s,2434
+flask_wtf/recaptcha/widgets.py,sha256=J_XyxAZt3uB15diIMnkXXGII2dmsWCsVsKV3KQYn4Ns,1512
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL
new file mode 100644
index 0000000..ba1a8af
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: hatchling 1.18.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst
new file mode 100644
index 0000000..63c3617
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst
@@ -0,0 +1,28 @@
+Copyright 2010 WTForms
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1.  Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+
+2.  Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+
+3.  Neither the name of the copyright holder nor the names of its
+    contributors may be used to endorse or promote products derived from
+    this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/venv/Lib/site-packages/flask_wtf/__init__.py b/venv/Lib/site-packages/flask_wtf/__init__.py
new file mode 100644
index 0000000..be2649e
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/__init__.py
@@ -0,0 +1,8 @@
+from .csrf import CSRFProtect
+from .form import FlaskForm
+from .form import Form
+from .recaptcha import Recaptcha
+from .recaptcha import RecaptchaField
+from .recaptcha import RecaptchaWidget
+
+__version__ = "1.2.1"
diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7c0573b1438c137de88c7976d43ddff37c7ebb80
GIT binary patch
literal 532
zcmY+AKTE?v7{>3Kq)A#@U33w_MWKV$R&)??sRRU3X%Qg=j+k6(Fn`MBTCtPg!p&9O
z{UDhnbP}9gMCjJZ_d;99dk^>ip6B2_?%ry(3WECheWjn!zlB*;X%5pazzcGbivtv5
z6Jx=ppcG1`l(PgZn{v)_poC?!oU;O~nrhBvV9nHkRa1Ah9%|@67zQ`+q}u6qjcXQj
z>Ts~S5!lIt5wj4CJnwp!I`)7&_cny}r7?UOxR9(YlUv{IQ$AK&&33bO*{DopCt)6w
z;Hru2!B8dxGDUcuErznP2vxCZ;-pH*h_b|wBSPNM6e2Xc>Kt1)38a?O-al*|w2!Q_
z*cpa2;)!)0+I^Z>BN~mY3;)had`@=<w)2q3o_G;?<lY`3e&lmPnuDjAb`r+!FrcSw
z73Pc2NXD@JNf_e{ZKg$KXd^8uL+fc#8QMyV%FuRNREBoaqQ2^D&sDKl9j^ZeCXJKS

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bf6047bf2e16901a5247250b9ec7f46df2d116ef
GIT binary patch
literal 698
zcmZWnJ#P~+7=G<b4!25M0fI<OFlOMRtzbwH3sDggWC2tuI>|D3PB?mB*q25@3^(ut
z*t#(mAsG1~htwhR%EW@MN(a;l`>q#M^|k!GAAbCPSpKrMwhCH){Q1oPYW>SNwKUI+
zh0w+Uc;KM`hJfJ-c=#iD7DtB`z&E`s8Db0^-Y$Kw!SNy1481Es`nF~?YTs*e?AhNh
zozH`Gr5Dg4zYT5R)?}m6nX_2e#(~Z?L>>%r$;ax(PQc{e?#rD%PXu>a>c`P86OkW{
z2OUchN+T9>O4TZ*VeCx;W49=MH(^0_@)Z`Q!s*~~*LfwmkdC``@BZfY)&r*>yOWSd
zsdS!(Y|N#z&!c_kng806e#&nr%-t))Bhv$&rlY&mjl+bccaoVf6PI8NxuGFju6cuG
zeX&wAl+A)En@MH+<0uyV{biji7NPvG%-q3>=;`=47Bgj6lXRMjOsiR?x^nRW%Ic~m
z{V)mm$PZF3)Ow}sYl4^_%0|quI3*9O)Q`kf?V6{TnTEnb2+d(5D_3D7v{JxD$Zu^Q
zoAAm1GXm&(Mvmaxr#Jb{?oa3)K`)2i9NH$@o;O-~do%AoFD#7kF=#k3P@IRd!+&#}
BvWNfx

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9a7b10bdeeef961ea8006709ee5d3d18fdcf34cd
GIT binary patch
literal 15040
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

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ff1baf217281277c65ce5ebaa756d3a5b1c90463
GIT binary patch
literal 8377
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

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..68c6e91088694c15d2928b11f2eebd165dd1d435
GIT binary patch
literal 7261
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

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2b7bd93f3fe31e810f7d0208a568d028cfbf0c82
GIT binary patch
literal 2060
zcmZ`(&2Jk;6rWkI?ag|lwAkPzZHcL>5>uKlAcTOBqE@OZsuW1!U|Fm--c90WckRxM
zU7BtyA9`?6fnHDoq9~^jNF{pUz@OnDOIRyKii9}yf?TC==!rMutYa(m?abTv-h93J
z&3o_1-d+^}{rdM0)>kq@e{v-t&;{+Sz;6|q$iy~sa0X*8OSa^MG9e*{K$bJIkYzjU
zL^2T}hwZ4NWE7AinI1EG2W3<VT|lOC2bnz^5<*+hTa<}O$ck-%vK5$6Ml;npl#V^-
zmNfRGv{Wn>2m>*eD-mM3OgD-}K0>T(C5!SAb<twFUC0@>1zN9TQEDt&R4*EADJ}Wh
z6=Jy5HdwykQeR^oTCROC2w~_U=zISHTSW|bKvXjE900)D&LecSEtfG7<18A?M_sGU
zkcl^7=Pkawpj*g8l?zM>bl8obE@(XxNS%Mycc67c#*RL3WD%S4!|>sgJ8!1<1gkv+
zz(-b$k1bXrE={o|E0rq%<T;iCfSL6<OLby5qNae(6^b`f3#8zr7HoqqpPe`6Ejx8R
z&z4fO;8-cAVCEO{xfb}XC9{55%<>c0*+PLhRPWr@kM3LHDI;dVG4gI&B^({XqtjQx
z56h+EEu-L<!5ASNSi;ftWxHUQgg*q~I1&w58WVm?ih-q`eQ<6%`w6uO&E_V?-Wq>n
zVlsQakSjS5S~Ppn;gORqTW&dfDL<d3d1jq08o6a*7FX24ztQ~olsi(q;UCp`>~;E2
z;&2MjFzNxTpnp`P#hVC+6OF!jl{F(s>HB86{?hBUC~v~}SA6in+qF~Ezn$NX&(z~H
zl}o$PcrAKtC;4(cdFGGg*zd`)9c|$5<$ISOT>aMm-d_9c(Zyy6^^ZS6Qos6?v#JJL
z55M_kw62`mjU{Vxau3c&InDy&0C3tH1h$GSl))y-K<q-AhD-^#Y=$5TWPxR_9pEsV
zBh35WhhixUwgRm?9B)aURFORFCOm1qV-`%@Cl7CceXBhN2jbbZB!Kgk(tfO^L%vKc
zd%=%TM)K~Wj~V3-K+KUB;P3Ot=RvuWxYCi)Bm35qFonJftb%s56WiKQT^p+6#=zj(
zK($;gZ{lCnpVdc`+r!iK;pv((O%gCS7t}wH56XcF3Y3GkzlUxtdvZnfAjy{nVuarw
zd(s^8<mVyaNnWTsF#!c>_Ax&sfTE}523UkiKMeQ^1t3bcLHtP3E)m1_u}k<{Ql1}x
zOn{9NtXz4q3!K1wny;A#)`iY)Osl@r7)q_rd{<s8-=DfawK-YS#&`RUe0}qan`=|+
zpKgA%-8WkA8?7m$0<hV1*jIGjH5^OVeO1>X-Agu?HC?|}GVIm}IR-}&{v<yku}-|8
z2r|K5;qFWbcPii#|C*8%ez%F*Oo@cYn&>cVZO2o8Hr-2N@E66v4~twCB|{VsKh{cK
zQF%mZ@U{IyGs2Tulng(r>xOOXx`@S=f4p)Kj@N<X&y@FC_k5q6fdT$z^dYcj2xHtp
z1GVnfKqqS5?f>SStf8*fK&NZnZC8D*5^Y8jxW9?oY$S#Om&5D{w>>==IF61SuX=ZH
M-@9GW_$O}jAF@~B5dZ)H

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/_compat.py b/venv/Lib/site-packages/flask_wtf/_compat.py
new file mode 100644
index 0000000..50973e0
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/_compat.py
@@ -0,0 +1,11 @@
+import warnings
+
+
+class FlaskWTFDeprecationWarning(DeprecationWarning):
+    pass
+
+
+warnings.simplefilter("always", FlaskWTFDeprecationWarning)
+warnings.filterwarnings(
+    "ignore", category=FlaskWTFDeprecationWarning, module="wtforms|flask_wtf"
+)
diff --git a/venv/Lib/site-packages/flask_wtf/csrf.py b/venv/Lib/site-packages/flask_wtf/csrf.py
new file mode 100644
index 0000000..06afa0c
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/csrf.py
@@ -0,0 +1,329 @@
+import hashlib
+import hmac
+import logging
+import os
+from urllib.parse import urlparse
+
+from flask import Blueprint
+from flask import current_app
+from flask import g
+from flask import request
+from flask import session
+from itsdangerous import BadData
+from itsdangerous import SignatureExpired
+from itsdangerous import URLSafeTimedSerializer
+from werkzeug.exceptions import BadRequest
+from wtforms import ValidationError
+from wtforms.csrf.core import CSRF
+
+__all__ = ("generate_csrf", "validate_csrf", "CSRFProtect")
+logger = logging.getLogger(__name__)
+
+
+def generate_csrf(secret_key=None, token_key=None):
+    """Generate a CSRF token. The token is cached for a request, so multiple
+    calls to this function will generate the same token.
+
+    During testing, it might be useful to access the signed token in
+    ``g.csrf_token`` and the raw token in ``session['csrf_token']``.
+
+    :param secret_key: Used to securely sign the token. Default is
+        ``WTF_CSRF_SECRET_KEY`` or ``SECRET_KEY``.
+    :param token_key: Key where token is stored in session for comparison.
+        Default is ``WTF_CSRF_FIELD_NAME`` or ``'csrf_token'``.
+    """
+
+    secret_key = _get_config(
+        secret_key,
+        "WTF_CSRF_SECRET_KEY",
+        current_app.secret_key,
+        message="A secret key is required to use CSRF.",
+    )
+    field_name = _get_config(
+        token_key,
+        "WTF_CSRF_FIELD_NAME",
+        "csrf_token",
+        message="A field name is required to use CSRF.",
+    )
+
+    if field_name not in g:
+        s = URLSafeTimedSerializer(secret_key, salt="wtf-csrf-token")
+
+        if field_name not in session:
+            session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest()
+
+        try:
+            token = s.dumps(session[field_name])
+        except TypeError:
+            session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest()
+            token = s.dumps(session[field_name])
+
+        setattr(g, field_name, token)
+
+    return g.get(field_name)
+
+
+def validate_csrf(data, secret_key=None, time_limit=None, token_key=None):
+    """Check if the given data is a valid CSRF token. This compares the given
+    signed token to the one stored in the session.
+
+    :param data: The signed CSRF token to be checked.
+    :param secret_key: Used to securely sign the token. Default is
+        ``WTF_CSRF_SECRET_KEY`` or ``SECRET_KEY``.
+    :param time_limit: Number of seconds that the token is valid. Default is
+        ``WTF_CSRF_TIME_LIMIT`` or 3600 seconds (60 minutes).
+    :param token_key: Key where token is stored in session for comparison.
+        Default is ``WTF_CSRF_FIELD_NAME`` or ``'csrf_token'``.
+
+    :raises ValidationError: Contains the reason that validation failed.
+
+    .. versionchanged:: 0.14
+        Raises ``ValidationError`` with a specific error message rather than
+        returning ``True`` or ``False``.
+    """
+
+    secret_key = _get_config(
+        secret_key,
+        "WTF_CSRF_SECRET_KEY",
+        current_app.secret_key,
+        message="A secret key is required to use CSRF.",
+    )
+    field_name = _get_config(
+        token_key,
+        "WTF_CSRF_FIELD_NAME",
+        "csrf_token",
+        message="A field name is required to use CSRF.",
+    )
+    time_limit = _get_config(time_limit, "WTF_CSRF_TIME_LIMIT", 3600, required=False)
+
+    if not data:
+        raise ValidationError("The CSRF token is missing.")
+
+    if field_name not in session:
+        raise ValidationError("The CSRF session token is missing.")
+
+    s = URLSafeTimedSerializer(secret_key, salt="wtf-csrf-token")
+
+    try:
+        token = s.loads(data, max_age=time_limit)
+    except SignatureExpired as e:
+        raise ValidationError("The CSRF token has expired.") from e
+    except BadData as e:
+        raise ValidationError("The CSRF token is invalid.") from e
+
+    if not hmac.compare_digest(session[field_name], token):
+        raise ValidationError("The CSRF tokens do not match.")
+
+
+def _get_config(
+    value, config_name, default=None, required=True, message="CSRF is not configured."
+):
+    """Find config value based on provided value, Flask config, and default
+    value.
+
+    :param value: already provided config value
+    :param config_name: Flask ``config`` key
+    :param default: default value if not provided or configured
+    :param required: whether the value must not be ``None``
+    :param message: error message if required config is not found
+    :raises KeyError: if required config is not found
+    """
+
+    if value is None:
+        value = current_app.config.get(config_name, default)
+
+    if required and value is None:
+        raise RuntimeError(message)
+
+    return value
+
+
+class _FlaskFormCSRF(CSRF):
+    def setup_form(self, form):
+        self.meta = form.meta
+        return super().setup_form(form)
+
+    def generate_csrf_token(self, csrf_token_field):
+        return generate_csrf(
+            secret_key=self.meta.csrf_secret, token_key=self.meta.csrf_field_name
+        )
+
+    def validate_csrf_token(self, form, field):
+        if g.get("csrf_valid", False):
+            # already validated by CSRFProtect
+            return
+
+        try:
+            validate_csrf(
+                field.data,
+                self.meta.csrf_secret,
+                self.meta.csrf_time_limit,
+                self.meta.csrf_field_name,
+            )
+        except ValidationError as e:
+            logger.info(e.args[0])
+            raise
+
+
+class CSRFProtect:
+    """Enable CSRF protection globally for a Flask app.
+
+    ::
+
+        app = Flask(__name__)
+        csrf = CSRFProtect(app)
+
+    Checks the ``csrf_token`` field sent with forms, or the ``X-CSRFToken``
+    header sent with JavaScript requests. Render the token in templates using
+    ``{{ csrf_token() }}``.
+
+    See the :ref:`csrf` documentation.
+    """
+
+    def __init__(self, app=None):
+        self._exempt_views = set()
+        self._exempt_blueprints = set()
+
+        if app:
+            self.init_app(app)
+
+    def init_app(self, app):
+        app.extensions["csrf"] = self
+
+        app.config.setdefault("WTF_CSRF_ENABLED", True)
+        app.config.setdefault("WTF_CSRF_CHECK_DEFAULT", True)
+        app.config["WTF_CSRF_METHODS"] = set(
+            app.config.get("WTF_CSRF_METHODS", ["POST", "PUT", "PATCH", "DELETE"])
+        )
+        app.config.setdefault("WTF_CSRF_FIELD_NAME", "csrf_token")
+        app.config.setdefault("WTF_CSRF_HEADERS", ["X-CSRFToken", "X-CSRF-Token"])
+        app.config.setdefault("WTF_CSRF_TIME_LIMIT", 3600)
+        app.config.setdefault("WTF_CSRF_SSL_STRICT", True)
+
+        app.jinja_env.globals["csrf_token"] = generate_csrf
+        app.context_processor(lambda: {"csrf_token": generate_csrf})
+
+        @app.before_request
+        def csrf_protect():
+            if not app.config["WTF_CSRF_ENABLED"]:
+                return
+
+            if not app.config["WTF_CSRF_CHECK_DEFAULT"]:
+                return
+
+            if request.method not in app.config["WTF_CSRF_METHODS"]:
+                return
+
+            if not request.endpoint:
+                return
+
+            if app.blueprints.get(request.blueprint) in self._exempt_blueprints:
+                return
+
+            view = app.view_functions.get(request.endpoint)
+            dest = f"{view.__module__}.{view.__name__}"
+
+            if dest in self._exempt_views:
+                return
+
+            self.protect()
+
+    def _get_csrf_token(self):
+        # find the token in the form data
+        field_name = current_app.config["WTF_CSRF_FIELD_NAME"]
+        base_token = request.form.get(field_name)
+
+        if base_token:
+            return base_token
+
+        # if the form has a prefix, the name will be {prefix}-csrf_token
+        for key in request.form:
+            if key.endswith(field_name):
+                csrf_token = request.form[key]
+
+                if csrf_token:
+                    return csrf_token
+
+        # find the token in the headers
+        for header_name in current_app.config["WTF_CSRF_HEADERS"]:
+            csrf_token = request.headers.get(header_name)
+
+            if csrf_token:
+                return csrf_token
+
+        return None
+
+    def protect(self):
+        if request.method not in current_app.config["WTF_CSRF_METHODS"]:
+            return
+
+        try:
+            validate_csrf(self._get_csrf_token())
+        except ValidationError as e:
+            logger.info(e.args[0])
+            self._error_response(e.args[0])
+
+        if request.is_secure and current_app.config["WTF_CSRF_SSL_STRICT"]:
+            if not request.referrer:
+                self._error_response("The referrer header is missing.")
+
+            good_referrer = f"https://{request.host}/"
+
+            if not same_origin(request.referrer, good_referrer):
+                self._error_response("The referrer does not match the host.")
+
+        g.csrf_valid = True  # mark this request as CSRF valid
+
+    def exempt(self, view):
+        """Mark a view or blueprint to be excluded from CSRF protection.
+
+        ::
+
+            @app.route('/some-view', methods=['POST'])
+            @csrf.exempt
+            def some_view():
+                ...
+
+        ::
+
+            bp = Blueprint(...)
+            csrf.exempt(bp)
+
+        """
+
+        if isinstance(view, Blueprint):
+            self._exempt_blueprints.add(view)
+            return view
+
+        if isinstance(view, str):
+            view_location = view
+        else:
+            view_location = ".".join((view.__module__, view.__name__))
+
+        self._exempt_views.add(view_location)
+        return view
+
+    def _error_response(self, reason):
+        raise CSRFError(reason)
+
+
+class CSRFError(BadRequest):
+    """Raise if the client sends invalid CSRF data with the request.
+
+    Generates a 400 Bad Request response with the failure reason by default.
+    Customize the response by registering a handler with
+    :meth:`flask.Flask.errorhandler`.
+    """
+
+    description = "CSRF validation failed."
+
+
+def same_origin(current_uri, compare_uri):
+    current = urlparse(current_uri)
+    compare = urlparse(compare_uri)
+
+    return (
+        current.scheme == compare.scheme
+        and current.hostname == compare.hostname
+        and current.port == compare.port
+    )
diff --git a/venv/Lib/site-packages/flask_wtf/file.py b/venv/Lib/site-packages/flask_wtf/file.py
new file mode 100644
index 0000000..a720dff
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/file.py
@@ -0,0 +1,147 @@
+from collections import abc
+
+from werkzeug.datastructures import FileStorage
+from wtforms import FileField as _FileField
+from wtforms import MultipleFileField as _MultipleFileField
+from wtforms.validators import DataRequired
+from wtforms.validators import StopValidation
+from wtforms.validators import ValidationError
+
+
+class FileField(_FileField):
+    """Werkzeug-aware subclass of :class:`wtforms.fields.FileField`."""
+
+    def process_formdata(self, valuelist):
+        valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x)
+        data = next(valuelist, None)
+
+        if data is not None:
+            self.data = data
+        else:
+            self.raw_data = ()
+
+
+class MultipleFileField(_MultipleFileField):
+    """Werkzeug-aware subclass of :class:`wtforms.fields.MultipleFileField`.
+
+    .. versionadded:: 1.2.0
+    """
+
+    def process_formdata(self, valuelist):
+        valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x)
+        data = list(valuelist) or None
+
+        if data is not None:
+            self.data = data
+        else:
+            self.raw_data = ()
+
+
+class FileRequired(DataRequired):
+    """Validates that the uploaded files(s) is a Werkzeug
+    :class:`~werkzeug.datastructures.FileStorage` object.
+
+    :param message: error message
+
+    You can also use the synonym ``file_required``.
+    """
+
+    def __call__(self, form, field):
+        field_data = [field.data] if not isinstance(field.data, list) else field.data
+        if not (
+            all(isinstance(x, FileStorage) and x for x in field_data) and field_data
+        ):
+            raise StopValidation(
+                self.message or field.gettext("This field is required.")
+            )
+
+
+file_required = FileRequired
+
+
+class FileAllowed:
+    """Validates that the uploaded file(s) is allowed by a given list of
+    extensions or a Flask-Uploads :class:`~flaskext.uploads.UploadSet`.
+
+    :param upload_set: A list of extensions or an
+        :class:`~flaskext.uploads.UploadSet`
+    :param message: error message
+
+    You can also use the synonym ``file_allowed``.
+    """
+
+    def __init__(self, upload_set, message=None):
+        self.upload_set = upload_set
+        self.message = message
+
+    def __call__(self, form, field):
+        field_data = [field.data] if not isinstance(field.data, list) else field.data
+        if not (
+            all(isinstance(x, FileStorage) and x for x in field_data) and field_data
+        ):
+            return
+
+        filenames = [f.filename.lower() for f in field_data]
+
+        for filename in filenames:
+            if isinstance(self.upload_set, abc.Iterable):
+                if any(filename.endswith("." + x) for x in self.upload_set):
+                    continue
+
+                raise StopValidation(
+                    self.message
+                    or field.gettext(
+                        "File does not have an approved extension: {extensions}"
+                    ).format(extensions=", ".join(self.upload_set))
+                )
+
+            if not self.upload_set.file_allowed(field_data, filename):
+                raise StopValidation(
+                    self.message
+                    or field.gettext("File does not have an approved extension.")
+                )
+
+
+file_allowed = FileAllowed
+
+
+class FileSize:
+    """Validates that the uploaded file(s) is within a minimum and maximum
+    file size (set in bytes).
+
+    :param min_size: minimum allowed file size (in bytes). Defaults to 0 bytes.
+    :param max_size: maximum allowed file size (in bytes).
+    :param message: error message
+
+    You can also use the synonym ``file_size``.
+    """
+
+    def __init__(self, max_size, min_size=0, message=None):
+        self.min_size = min_size
+        self.max_size = max_size
+        self.message = message
+
+    def __call__(self, form, field):
+        field_data = [field.data] if not isinstance(field.data, list) else field.data
+        if not (
+            all(isinstance(x, FileStorage) and x for x in field_data) and field_data
+        ):
+            return
+
+        for f in field_data:
+            file_size = len(f.read())
+            f.seek(0)  # reset cursor position to beginning of file
+
+            if (file_size < self.min_size) or (file_size > self.max_size):
+                # the file is too small or too big => validation failure
+                raise ValidationError(
+                    self.message
+                    or field.gettext(
+                        "File must be between {min_size} and {max_size} bytes.".format(
+                            min_size=self.min_size, max_size=self.max_size
+                        )
+                    )
+                )
+
+
+file_size = FileSize
diff --git a/venv/Lib/site-packages/flask_wtf/form.py b/venv/Lib/site-packages/flask_wtf/form.py
new file mode 100644
index 0000000..c7f52e0
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/form.py
@@ -0,0 +1,127 @@
+from flask import current_app
+from flask import request
+from flask import session
+from markupsafe import Markup
+from werkzeug.datastructures import CombinedMultiDict
+from werkzeug.datastructures import ImmutableMultiDict
+from werkzeug.utils import cached_property
+from wtforms import Form
+from wtforms.meta import DefaultMeta
+from wtforms.widgets import HiddenInput
+
+from .csrf import _FlaskFormCSRF
+
+try:
+    from .i18n import translations
+except ImportError:
+    translations = None  # babel not installed
+
+
+SUBMIT_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
+_Auto = object()
+
+
+class FlaskForm(Form):
+    """Flask-specific subclass of WTForms :class:`~wtforms.form.Form`.
+
+    If ``formdata`` is not specified, this will use :attr:`flask.request.form`
+    and :attr:`flask.request.files`.  Explicitly pass ``formdata=None`` to
+    prevent this.
+    """
+
+    class Meta(DefaultMeta):
+        csrf_class = _FlaskFormCSRF
+        csrf_context = session  # not used, provided for custom csrf_class
+
+        @cached_property
+        def csrf(self):
+            return current_app.config.get("WTF_CSRF_ENABLED", True)
+
+        @cached_property
+        def csrf_secret(self):
+            return current_app.config.get("WTF_CSRF_SECRET_KEY", current_app.secret_key)
+
+        @cached_property
+        def csrf_field_name(self):
+            return current_app.config.get("WTF_CSRF_FIELD_NAME", "csrf_token")
+
+        @cached_property
+        def csrf_time_limit(self):
+            return current_app.config.get("WTF_CSRF_TIME_LIMIT", 3600)
+
+        def wrap_formdata(self, form, formdata):
+            if formdata is _Auto:
+                if _is_submitted():
+                    if request.files:
+                        return CombinedMultiDict((request.files, request.form))
+                    elif request.form:
+                        return request.form
+                    elif request.is_json:
+                        return ImmutableMultiDict(request.get_json())
+
+                return None
+
+            return formdata
+
+        def get_translations(self, form):
+            if not current_app.config.get("WTF_I18N_ENABLED", True):
+                return super().get_translations(form)
+
+            return translations
+
+    def __init__(self, formdata=_Auto, **kwargs):
+        super().__init__(formdata=formdata, **kwargs)
+
+    def is_submitted(self):
+        """Consider the form submitted if there is an active request and
+        the method is ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
+        """
+
+        return _is_submitted()
+
+    def validate_on_submit(self, extra_validators=None):
+        """Call :meth:`validate` only if the form is submitted.
+        This is a shortcut for ``form.is_submitted() and form.validate()``.
+        """
+        return self.is_submitted() and self.validate(extra_validators=extra_validators)
+
+    def hidden_tag(self, *fields):
+        """Render the form's hidden fields in one call.
+
+        A field is considered hidden if it uses the
+        :class:`~wtforms.widgets.HiddenInput` widget.
+
+        If ``fields`` are given, only render the given fields that
+        are hidden.  If a string is passed, render the field with that
+        name if it exists.
+
+        .. versionchanged:: 0.13
+
+           No longer wraps inputs in hidden div.
+           This is valid HTML 5.
+
+        .. versionchanged:: 0.13
+
+           Skip passed fields that aren't hidden.
+           Skip passed names that don't exist.
+        """
+
+        def hidden_fields(fields):
+            for f in fields:
+                if isinstance(f, str):
+                    f = getattr(self, f, None)
+
+                if f is None or not isinstance(f.widget, HiddenInput):
+                    continue
+
+                yield f
+
+        return Markup("\n".join(str(f) for f in hidden_fields(fields or self)))
+
+
+def _is_submitted():
+    """Consider the form submitted if there is an active request and
+    the method is ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
+    """
+
+    return bool(request) and request.method in SUBMIT_METHODS
diff --git a/venv/Lib/site-packages/flask_wtf/i18n.py b/venv/Lib/site-packages/flask_wtf/i18n.py
new file mode 100644
index 0000000..1cc0e9c
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/i18n.py
@@ -0,0 +1,47 @@
+from babel import support
+from flask import current_app
+from flask import request
+from flask_babel import get_locale
+from wtforms.i18n import messages_path
+
+__all__ = ("Translations", "translations")
+
+
+def _get_translations():
+    """Returns the correct gettext translations.
+    Copy from flask-babel with some modifications.
+    """
+
+    if not request:
+        return None
+
+    # babel should be in extensions for get_locale
+    if "babel" not in current_app.extensions:
+        return None
+
+    translations = getattr(request, "wtforms_translations", None)
+
+    if translations is None:
+        translations = support.Translations.load(
+            messages_path(), [get_locale()], domain="wtforms"
+        )
+        request.wtforms_translations = translations
+
+    return translations
+
+
+class Translations:
+    def gettext(self, string):
+        t = _get_translations()
+        return string if t is None else t.ugettext(string)
+
+    def ngettext(self, singular, plural, n):
+        t = _get_translations()
+
+        if t is None:
+            return singular if n == 1 else plural
+
+        return t.ungettext(singular, plural, n)
+
+
+translations = Translations()
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py b/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py
new file mode 100644
index 0000000..3100d37
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py
@@ -0,0 +1,3 @@
+from .fields import RecaptchaField
+from .validators import Recaptcha
+from .widgets import RecaptchaWidget
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3f546fdcc15a0e8960fc72d690940a98e3e7e797
GIT binary patch
literal 380
zcmZ3^%ge<81Y6$COU(k(k3k$5V1hC}YXKS48B!Qh7;_kM8KW2(L2RZRrd;MIW-yyM
zhb5OaiWSIa31-k_eaQ&apvicPFDNxRv7jV5Bhf80H75lq!U-1va`@q#@XVC-)Dk~U
zwp(mzAeF^M%s>-vag`<JWTqsR<QEkev48~F%fSkZi&%lYl?<POB*U*rXRDad;?$zz
zm}DaZQ$u4Tvly5B<kH;KyprM=&)mfH)Z&=3)V#77pUk9~;>?m%-GapAY_M=zPGWI(
ze0fQlei6*2`tk9Zd6^~g@p=W7zc_4i^HWN5QtgU(fHr}ARqO~PJ}@&fGTvZNx`2vq
TFeqL?MK>5!E?`4Npg;!zO^j@W

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2448d610b0c6db964cd16355a47e1ca99fa9e123
GIT binary patch
literal 1134
zcmb7CL2DC16rS15W;bcmq7quDXoXV9!M0Y~Ls1cxN@<CpmckMkx4V<L?QRlhHjPSz
z912EI@gnsk6>3#_68s?%dI)<ec<MzW@gP0<X15y*dhzY-x9{z|dGoz*=0hr#L||XO
zzonlsLf=FXnR4uOmcZFU7P7E|T&!b^EXk4`$(40E@MBKQRdfY>rGWLggl-}$z6lY<
zyD~z1@P?+VmU;(ev_lbQu&>P9)Ugg>XA!AY+bU3xgPXWbO{46Y_YHrb>jueYSSiG)
zaOWDtPk|&wI<}B5Nr=j8u_$#HoV_SOx?%xIE7m}HY{{DmU2UL;Sl57<!c<7h`pAqb
zi&a91K6pF#0AAxQRFldBo*;c}z4Z(D3GDCvQK5H!O^Vjla81ffHmb=x_@$5)0SBDT
z$Se*izQQYI%6yFwyJ&laWE4Nfsgw5=$5^F~pWHAU+cLZo<9>X-YOn(L6NH$K!8svZ
z&>jV=AFW)U%Pn)tc+Q-jnz?XsdNy~XWL8{S^muN;H42pHHfV7pw`8y8xb4yNWy4$#
ze&?asI;nd3Nfw>RWL_K^pC~_K{jkO3q22?Ui0BT&B%cHLRY$G#*=Bm`{ldG&oyG0T
zjbuw3exW>5pR4=Y@SF5@vZ>9$_?QpBUF^VWl#y5hKG{q{NKrIgN{F8%#4TABNBI4O
zJg68>IKxES{$Q8WN^qzAczCz`!EPIbGFD<i3x4WYHIq6H=#1G3D8j`FUu7o&gKH6h
zV8ZVLKvU7vN{uyE_ETeDQ{(%o@%nr_j)q3+x7smDnQf#00CoxrXET~V0D_m8%O^sq
znYc@V2tjvDC>vf8l@LQv4!C_nL4knrk#;H6=L$Om*`gI*1!&6{;}#li_KpLTZuX8A
hI@jzS2Yn;W<mtNFj%yhD`Ws#?{}6wF{$na2_8aiZ8OZ<u

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..daaff04283480f89350a9442f17337effb27f9bc
GIT binary patch
literal 3739
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

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c2e05915fccf962c1146aeba49c1a004f3638e79
GIT binary patch
literal 2875
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
OejvR6{vUxIhW`Ny>aUvs

literal 0
HcmV?d00001

diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py b/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py
new file mode 100644
index 0000000..e91fd09
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py
@@ -0,0 +1,17 @@
+from wtforms.fields import Field
+
+from . import widgets
+from .validators import Recaptcha
+
+__all__ = ["RecaptchaField"]
+
+
+class RecaptchaField(Field):
+    widget = widgets.RecaptchaWidget()
+
+    # error message if recaptcha validation fails
+    recaptcha_error = None
+
+    def __init__(self, label="", validators=None, **kwargs):
+        validators = validators or [Recaptcha()]
+        super().__init__(label, validators, **kwargs)
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py b/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py
new file mode 100644
index 0000000..c5cafb3
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py
@@ -0,0 +1,75 @@
+import json
+from urllib import request as http
+from urllib.parse import urlencode
+
+from flask import current_app
+from flask import request
+from wtforms import ValidationError
+
+RECAPTCHA_VERIFY_SERVER_DEFAULT = "https://www.google.com/recaptcha/api/siteverify"
+RECAPTCHA_ERROR_CODES = {
+    "missing-input-secret": "The secret parameter is missing.",
+    "invalid-input-secret": "The secret parameter is invalid or malformed.",
+    "missing-input-response": "The response parameter is missing.",
+    "invalid-input-response": "The response parameter is invalid or malformed.",
+}
+
+
+__all__ = ["Recaptcha"]
+
+
+class Recaptcha:
+    """Validates a ReCaptcha."""
+
+    def __init__(self, message=None):
+        if message is None:
+            message = RECAPTCHA_ERROR_CODES["missing-input-response"]
+        self.message = message
+
+    def __call__(self, form, field):
+        if current_app.testing:
+            return True
+
+        if request.is_json:
+            response = request.json.get("g-recaptcha-response", "")
+        else:
+            response = request.form.get("g-recaptcha-response", "")
+        remote_ip = request.remote_addr
+
+        if not response:
+            raise ValidationError(field.gettext(self.message))
+
+        if not self._validate_recaptcha(response, remote_ip):
+            field.recaptcha_error = "incorrect-captcha-sol"
+            raise ValidationError(field.gettext(self.message))
+
+    def _validate_recaptcha(self, response, remote_addr):
+        """Performs the actual validation."""
+        try:
+            private_key = current_app.config["RECAPTCHA_PRIVATE_KEY"]
+        except KeyError:
+            raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set") from None
+
+        verify_server = current_app.config.get("RECAPTCHA_VERIFY_SERVER")
+        if not verify_server:
+            verify_server = RECAPTCHA_VERIFY_SERVER_DEFAULT
+
+        data = urlencode(
+            {"secret": private_key, "remoteip": remote_addr, "response": response}
+        )
+
+        http_response = http.urlopen(verify_server, data.encode("utf-8"))
+
+        if http_response.code != 200:
+            return False
+
+        json_resp = json.loads(http_response.read())
+
+        if json_resp["success"]:
+            return True
+
+        for error in json_resp.get("error-codes", []):
+            if error in RECAPTCHA_ERROR_CODES:
+                raise ValidationError(RECAPTCHA_ERROR_CODES[error])
+
+        return False
diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py b/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py
new file mode 100644
index 0000000..bfae830
--- /dev/null
+++ b/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py
@@ -0,0 +1,43 @@
+from urllib.parse import urlencode
+
+from flask import current_app
+from markupsafe import Markup
+
+RECAPTCHA_SCRIPT_DEFAULT = "https://www.google.com/recaptcha/api.js"
+RECAPTCHA_DIV_CLASS_DEFAULT = "g-recaptcha"
+RECAPTCHA_TEMPLATE = """
+<script src='%s' async defer></script>
+<div class="%s" %s></div>
+"""
+
+__all__ = ["RecaptchaWidget"]
+
+
+class RecaptchaWidget:
+    def recaptcha_html(self, public_key):
+        html = current_app.config.get("RECAPTCHA_HTML")
+        if html:
+            return Markup(html)
+        params = current_app.config.get("RECAPTCHA_PARAMETERS")
+        script = current_app.config.get("RECAPTCHA_SCRIPT")
+        if not script:
+            script = RECAPTCHA_SCRIPT_DEFAULT
+        if params:
+            script += "?" + urlencode(params)
+        attrs = current_app.config.get("RECAPTCHA_DATA_ATTRS", {})
+        attrs["sitekey"] = public_key
+        snippet = " ".join(f'data-{k}="{attrs[k]}"' for k in attrs)  # noqa: B028, B907
+        div_class = current_app.config.get("RECAPTCHA_DIV_CLASS")
+        if not div_class:
+            div_class = RECAPTCHA_DIV_CLASS_DEFAULT
+        return Markup(RECAPTCHA_TEMPLATE % (script, div_class, snippet))
+
+    def __call__(self, field, error=None, **kwargs):
+        """Returns the recaptcha input HTML."""
+
+        try:
+            public_key = current_app.config["RECAPTCHA_PUBLIC_KEY"]
+        except KeyError:
+            raise RuntimeError("RECAPTCHA_PUBLIC_KEY config not set") from None
+
+        return self.recaptcha_html(public_key)
-- 
GitLab