Coverage report: + 84% +
+ + ++ Files + Functions + Classes +
++ coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +
+diff --git a/.coverage b/.coverage new file mode 100644 index 000000000..44fe1534a Binary files /dev/null and b/.coverage differ diff --git a/.gitignore b/.gitignore index 2cba99d87..e7e28eb67 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ bin include lib .Python -tests/ .envrc -__pycache__ \ No newline at end of file +__pycache__ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 61307d2cd..8d3def852 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,256 @@ -# gudlift-registration +# Flask App - OpenClassrooms Project 11 +**Improve a web Flask app by testing and debugging** -1. Why +--- +## DESCRIPTION - This is a proof of concept (POC) project to show a light-weight version of our competition booking platform. The aim is the keep things as light as possible, and use feedback from the users to iterate. +This project was completed as part of the "Python Developer" path at OpenClassrooms. -2. Getting Started +The goal was to improve a web application, through testing and debugging, capable of: - This project uses the following technologies: +- managing lifting competitions +- allowing clubs to authenticate themselves within the application +- allowing clubs to reserve places in several lifting competitions - * Python v3.x+ +The application must: - * [Flask](https://flask.palletsprojects.com/en/1.1.x/) +- allow the club secretary to log in. +- allow the club secretary to view competitions and other clubs' points. +- allow the club secretary to use their points to purchase competition tickets. +- display error messages in case of incorrect action, according to the application specifications. - Whereas Django does a lot of things for us out of the box, Flask allows us to add only what we need. - +--- - * [Virtual environment](https://virtualenv.pypa.io/en/stable/installation.html) +## PROJECT STRUCTURE +
+
+
virtualenv .. This will then set up a a virtual python environment within that directory.
+- ### Navigate into the project directory :
+ `cd OpenClassrooms_Project_11`
- - Next, type source bin/activate. You should see that your command prompt has changed to the name of the folder. This means that you can install packages in here without affecting affecting files outside. To deactivate, type deactivate
+- ### Create a virtual environment and dependencies :
- - Rather than hunting around for the packages you need, you can install in one step. Type pip install -r requirements.txt. This will install all the packages listed in the respective file. If you install a package, make sure others know by updating the requirements.txt file. An easy way to do this is pip freeze > requirements.txt
+1. #### With [uv](https://docs.astral.sh/uv/)
- - Flask requires that you set an environmental variable to the python file. However you do that, you'll want to set the file to be server.py. Check [here](https://flask.palletsprojects.com/en/1.1.x/quickstart/#a-minimal-application) for more details
+ `uv` is an environment and dependencies manager.
+
+ - #### Install environment and dependencies
+
+ `uv sync`
- - You should now be ready to test the application. In the directory, type either flask run or python -m flask run. The app should respond with an address you should be able to go to using your browser.
+2. #### With pip
-4. Current Setup
+ - #### Install the virtual env :
- The app is powered by [JSON files](https://www.tutorialspoint.com/json/json_quick_guide.htm). This is to get around having a DB until we actually need one. The main ones are:
-
- * competitions.json - list of competitions
- * clubs.json - list of clubs with relevant information. You can look here to see what email addresses the app will accept for login.
+ `python -m venv env`
-5. Testing
+ - #### Activate the virtual env :
+ `source env/bin/activate`
+ Or
+ `env\Scripts\activate` on Windows
- You are free to use whatever testing framework you like-the main thing is that you can show what tests you are using.
+3. #### With [Poetry](https://python-poetry.org/docs/)
- We also like to show how well we're testing, so there's a module called
- [coverage](https://coverage.readthedocs.io/en/coverage-5.1/) you should add to your project.
+ `Poetry` is a tool for dependency management and packaging in Python.
+
+ - #### Install the virtual env :
+ `py -3.13 -m venv env`
+
+ - #### Activate the virtual env :
+ `poetry env activate`
+- ### Install dependencies
+ 1. #### With [uv](https://docs.astral.sh/uv/)
+ `uv sync` or `uv pip install -r requirements.txt`
+
+ 2. #### With pip
+ `pip install -r requirements.txt`
+
+ 3. #### With [Poetry](https://python-poetry.org/docs/)
+ `poetry install`
+
+ (NB : Poetry and uv will read the `pyproject.toml` file to know which dependencies to install)
+
+---
+
+## USAGE
+
+### Launching server
+- Open a terminal
+- Go to project folder - example : `cd gudlft`
+- Activate the virtual environment as described previously
+- Create flask environment :
+ - With PowerShell :
+ ```
+ $env:FLASK_APP = "server"
+ ```
+ Plus, in option, the 2 lines below for the good execution of performance tests
+ ```
+ $env:CLUBS_JSON="D:\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +
+| File | +class | ++ | statements | +missing | +excluded | ++ | coverage | +
|---|---|---|---|---|---|---|---|
| server.py | +(no class) | ++ | 151 | +24 | +0 | ++ | 84% | +
| Total | ++ | + | 151 | +24 | +0 | ++ | 84% | +
+ No items found using the specified filter. +
++ coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +
+| File | +function | ++ | statements | +missing | +excluded | ++ | coverage | +
|---|---|---|---|---|---|---|---|
| server.py | +index | ++ | 1 | +0 | +0 | ++ | 100% | +
| server.py | +sign_up | ++ | 1 | +0 | +0 | ++ | 100% | +
| server.py | +profile | ++ | 8 | +2 | +0 | ++ | 75% | +
| server.py | +profile_post | ++ | 30 | +9 | +0 | ++ | 70% | +
| server.py | +change_password | ++ | 23 | +6 | +0 | ++ | 74% | +
| server.py | +show_summary | ++ | 5 | +2 | +0 | ++ | 60% | +
| server.py | +show_summary_post | ++ | 15 | +3 | +0 | ++ | 80% | +
| server.py | +book | ++ | 15 | +2 | +0 | ++ | 87% | +
| server.py | +purchase_places | ++ | 12 | +0 | +0 | ++ | 100% | +
| server.py | +points_board | ++ | 3 | +0 | +0 | ++ | 100% | +
| server.py | +logout | ++ | 4 | +0 | +0 | ++ | 100% | +
| server.py | +(no function) | ++ | 34 | +0 | +0 | ++ | 100% | +
| Total | ++ | + | 151 | +24 | +0 | ++ | 84% | +
+ No items found using the specified filter. +
++ coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +
+| File | ++ | statements | +missing | +excluded | ++ | coverage | +
|---|---|---|---|---|---|---|
| server.py | ++ | 151 | +24 | +0 | ++ | 84% | +
| Total | ++ | 151 | +24 | +0 | ++ | 84% | +
+ No items found using the specified filter. +
++ « prev + ^ index + » next + + coverage.py v7.13.4, + created at 2026-04-07 14:49 +0200 +
+ +1import os
+ +3from flask import Flask, render_template, request, redirect, flash, url_for, session
+4from werkzeug.security import generate_password_hash, check_password_hash
+ + +7app = Flask(__name__)
+8app.secret_key = 'something_special'
+ +10app.config["CLUBS_JSON"] = os.environ.get("CLUBS_JSON")
+11app.config["COMPETITIONS_JSON"] = os.environ.get("COMPETITIONS_JSON")
+ +13CLUB_POINTS = 15
+ +15import utils
+16import exceptions
+ +18utils.clubs = utils.load_clubs()
+19utils.competitions = utils.load_competitions()
+ + +22@app.route('/')
+23def index():
+24 """
+25 Route to home page
+26 Returns:
+27 The template for home page
+28 """
+29 return render_template(template_name_or_list='index.html')
+ + +32@app.route('/signUp')
+33def sign_up():
+34 """
+35 Route to sign up page
+36 Returns:
+37 The template for sign up page
+38 """
+39 return render_template(template_name_or_list='sign_up.html')
+ + +42@app.route('/profile/<club>', methods=['GET'])
+43def profile(club):
+44 """
+45 Route to club profile page
+46 Args:
+47 club (str): The club name
+ +49 Returns:
+50 The template for club profile page. The template may display a message to the user.
+51 """
+52 if "club" in session and session['club'] == club:
+53 the_club = next((c for c in utils.clubs if c['name'] == club), None)
+ +55 if the_club is None:
+56 flash(message="Sorry, that club was not found.")
+57 return render_template(template_name_or_list="index.html", error="Club not found"), 404
+ +59 return render_template(template_name_or_list='profile.html', club=the_club)
+ +61 flash(message="Sorry, you are not allow to see that profile.")
+62 return render_template(template_name_or_list='index.html', error="Not allow"), 403
+ + +65@app.route('/profile', methods=['POST'])
+66def profile_post():
+67 """
+68 Route for signing up.
+69 Returns:
+70 The template to profile page if correctly signed up or sign up page otherwise.
+71 """
+72 club_name = request.form['name']
+73 club_email = request.form['email']
+74 club_password = request.form['password']
+75 club_password_confirmation = request.form['confirm_password']
+ +77 try:
+78 utils.validate_profile_fields(club_name, club_email, club_password, club_password_confirmation)
+ +80 except exceptions.ValidationError as e:
+81 flash(message=e.message)
+82 return redirect(location=url_for('sign_up'))
+ +84 try:
+85 utils.validate_email_format(club_email)
+ +87 except exceptions.ValidationError as e:
+88 flash(message=e.message)
+89 return redirect(location=url_for('sign_up'))
+ +91 found_clubs = [c for c in utils.clubs if c['email'] == club_email or c['name'] == club_name]
+ +93 if not found_clubs:
+94 if club_password != club_password_confirmation:
+95 flash(message='Sorry, passwords do not match')
+96 return redirect(location=url_for('sign_up'))
+ +98 hashed_password = generate_password_hash(club_password)
+99 utils.add_club(name=club_name,
+100 email=club_email,
+101 password=hashed_password,
+102 points=str(CLUB_POINTS))
+ +104 the_club = next((c for c in utils.clubs if c['email'] == club_email), None)
+ +106 if the_club is None:
+107 flash(message="Sorry, something went wrong. Please try again.")
+108 return render_template(template_name_or_list='sign_up.html')
+ +110 flash(message="Great! You have successfully signed up.")
+111 session['club'] = the_club['name']
+112 return render_template(template_name_or_list='profile.html', club=the_club)
+ +114 else:
+115 flash(message="Sorry, the club already exists.")
+116 return render_template(template_name_or_list='sign_up.html')
+ + +119@app.route('/changePassword/<club>', methods=['GET', 'POST'])
+120def change_password(club):
+121 """
+122 Route to change password.
+123 Args:
+124 club (str): The club name
+ +126 Returns:
+127 The template for club profile page. The index template otherwise.
+128 """
+129 if "club" in session and session['club'] == club:
+130 if request.method == 'GET':
+131 the_club = next((c for c in utils.clubs if c['name'] == club), None)
+ +133 if the_club is None:
+134 flash(message="Sorry, that club was not found.")
+135 return render_template(template_name_or_list="index.html", error="Email not found"), 404
+ +137 return render_template(template_name_or_list='change_password.html', club=the_club)
+138 else:
+139 club_password = request.form['password']
+140 club_password_confirmation = request.form['confirm_password']
+ +142 the_club = next((c for c in utils.clubs if c['name'] == club), None)
+ +144 try:
+145 utils.validate_password(password=club_password,
+146 password2=club_password_confirmation,
+147 club=the_club)
+148 except exceptions.ValidationError as e:
+149 flash(e.message)
+150 return render_template(template_name_or_list='change_password.html',
+151 club=the_club,
+152 error=e.tag), 200
+ +154 the_club = utils.update_club_password(the_club, club_password)
+ +156 if the_club:
+157 flash(message="Great! You have successfully changed your password.")
+158 return render_template(template_name_or_list='profile.html', club=the_club)
+ +160 flash(message="Sorry, something went wrong. Please try again.")
+161 return render_template(template_name_or_list='index.html')
+ +163 flash(message="Sorry, you are not allow to do this action.")
+164 return render_template(template_name_or_list='index.html', error="Not allow"), 403
+ + +167@app.route('/showSummary/<club>', methods=['GET'])
+168def show_summary(club):
+169 """
+170 Route to club summary page
+171 Args:
+172 club (str): The club name
+ +174 Returns:
+175 The welcome template if authorized. The index template otherwise.
+176 """
+177 if "club" in session and session['club'] == club:
+178 the_club = next((c for c in utils.clubs if c['name'] == club), None)
+ +180 return render_template(template_name_or_list='welcome.html',
+181 club=the_club,
+182 competitions=utils.competitions)
+ +184 flash(message="Sorry, you are not allow to do this action.")
+185 return render_template(template_name_or_list='index.html', error="Not allow"), 403
+ + +188@app.route('/showSummary', methods=['POST'])
+189def show_summary_post():
+190 """
+191 Route to log in.
+192 Returns:
+193 The welcome template if success. The index template otherwise.
+194 """
+195 try:
+196 utils.validate_login_fields(request.form['email'], request.form['password'])
+197 except exceptions.ValidationError as e:
+198 flash(message=e.message)
+199 return render_template(template_name_or_list="index.html", error=e.tag), 404
+ +201 the_club = next((c for c in utils.clubs if c['email'] == request.form['email']), None)
+ +203 if the_club is None:
+204 flash(message="Sorry, that email was not found.")
+205 return render_template(template_name_or_list="index.html", error="Email not found"), 404
+ +207 if not check_password_hash(the_club['password'], request.form['password']):
+208 flash(message="Sorry, the password is incorrect.")
+209 return render_template(template_name_or_list="index.html", error="Incorrect password"), 403
+ +211 session["club"] = the_club["name"]
+ +213 flash("Great! You are successfully logged in.")
+214 return render_template(template_name_or_list='welcome.html',
+215 club=the_club,
+216 competitions=utils.competitions)
+ + +219@app.route('/book/<competition>/<club>')
+220def book(competition, club):
+221 """
+222 Route to book page
+223 Args:
+224 competition (str): The competition name
+225 club (str): The club name
+ +227 Returns:
+228 The booking template if success. The welcome template if error. The index template otherwise.
+229 """
+230 if "club" in session and session['club'] == club:
+231 found_club = [c for c in utils.clubs if c['name'] == club][0]
+232 found_competition = [c for c in utils.competitions if c['name'] == competition][0]
+ +234 try:
+235 utils.validate_competition(the_competition=found_competition)
+ +237 except exceptions.ValidationError as e:
+238 flash(message=e.message)
+ +240 the_club = next((a_club for a_club in utils.clubs if a_club['name'] == club), None)
+ +242 return render_template(template_name_or_list='welcome.html',
+243 club=the_club,
+244 competitions=utils.competitions,
+245 error=e.tag), 200
+ +247 if found_club and found_competition:
+248 return render_template(template_name_or_list='booking.html',
+249 club=found_club,
+250 competition=found_competition), 200
+251 else:
+252 flash(message="Sorry, something went wrong. Please try again.")
+253 return render_template(template_name_or_list='welcome.html',
+254 club=club,
+255 competitions=utils.competitions)
+ +257 flash(message="Sorry, you are not allow to do this action.")
+258 return render_template(template_name_or_list='index.html', error="Not allow"), 403
+ + +261@app.route('/purchasePlaces', methods=['POST'])
+262def purchase_places():
+263 """
+264 Route to purchase places page
+265 Returns:
+266 The welcome template if success. The welcome template with error message otherwise.
+267 """
+268 competition = [c for c in utils.competitions if c['name'] == request.form['competition']][0]
+269 club = [c for c in utils.clubs if c['name'] == request.form['club']][0]
+ +271 places_required = int(request.form['places']) if request.form['places'] else 0
+ +273 try:
+274 utils.validate_places(places_required=places_required,
+275 club=club,
+276 the_competition=competition)
+ +278 except exceptions.ValidationError as e:
+279 flash(message=e.message)
+280 return render_template(template_name_or_list='welcome.html',
+281 club=club,
+282 competitions=utils.competitions,
+283 error=e.tag), 200
+ +285 utils.update_club_booked_places(club=club,
+286 places=places_required,
+287 competition_name=competition["name"])
+ +289 utils.update_competition_available_places(the_competition=competition, places=places_required)
+ +291 flash(message=f"""
+292 Great! Booking of {places_required} place(s) for {competition['name']} competition complete!
+293 """)
+ +295 return render_template(template_name_or_list='welcome.html',
+296 club=club,
+297 competitions=utils.competitions)
+ + +300@app.route('/pointsBoard')
+301def points_board():
+302 """
+303 Route to points board page
+304 Returns:
+305 The points board template.
+306 """
+307 clubs_for_board = utils.copy_clubs_for_board()
+ +309 club = session.get('club')
+ +311 return render_template(template_name_or_list='points_board.html',
+312 clubs=clubs_for_board,
+313 club=club)
+ + +316@app.route('/logout')
+317def logout():
+318 """
+319 Route for logging out
+320 Returns:
+321 The index template.
+322 """
+323 flash(message="Great! You are successfully logged out.")
+324 if 'club' in session:
+325 session.pop('club')
+326 return redirect(location=url_for('index'))
+Generated on 2026-04-07 14:08 + with Installed plugins: flake8-html: 0.4.3, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0 +
+No flake8 errors found in 14 files scanned.
+
+ Club |
+ Points |
+
+ {% for club in clubs %}
+
{{ club.name }} |
+ {{ club.points }} |
+
Already happened
+ {% else %} + {%if comp['number_of_places']|int >0%} + Book Places + {% else %} +Sold out
+ {% endif %} + {% endif %} +