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/clubs.json b/clubs.json index 1d7ad1ffe..05d4126c7 100644 --- a/clubs.json +++ b/clubs.json @@ -1,16 +1,19 @@ -{"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", + "points": "13" + }, + { + "name": "She Lifts", + "email": "kate@shelifts.co.uk", + "points": "12" + }, + { + "name": "Iron Temple", + "email": "admin@irontemple.com", + "points": "4" + } + ] +} \ No newline at end of file diff --git a/competitions.json b/competitions.json index 039fc61bd..7c11ce528 100644 --- a/competitions.json +++ b/competitions.json @@ -1,14 +1,14 @@ { "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" + }, + { + "name": "Spring Festival", + "date": "2020-03-27 10:00:00", + "numberOfPlaces": "25" } ] } \ No newline at end of file diff --git a/server.py b/server.py index 4084baeac..9a36e20e9 100644 --- a/server.py +++ b/server.py @@ -13,21 +13,54 @@ def loadCompetitions(): listOfCompetitions = json.load(comps)['competitions'] return listOfCompetitions - app = Flask(__name__) app.secret_key = 'something_special' competitions = loadCompetitions() clubs = loadClubs() +def update_club_booked_places(club, places, competition_name): + clubs.remove(club) + + club["points"] = str(int(club["points"]) - places) + + clubs.append(club) + save_clubs() + +def save_clubs(): + with open('clubs.json', 'w') as c: + listOfClubs = {"clubs": clubs} + json.dump(listOfClubs, c, indent=4) + +def update_competition_available_places(competition, places): + competitions.remove(competition) + + competition['numberOfPlaces'] = str(int(competition['numberOfPlaces']) - places) + + competitions.append(competition) + save_competitions() + +def save_competitions(): + with open('competitions.json', 'w') as comps: + listOfCompetitions = {"competitions": competitions} + json.dump(listOfCompetitions, comps, indent=4) + @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) + + club = next((club for club in clubs if club['email'] == request.form['email']), None) + + if club is None: + flash("Sorry, that email was not found.") + return render_template(template_name_or_list="index.html", error="Email not found"), 404 + + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) @app.route('/book//') @@ -35,10 +68,14 @@ 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) + return render_template(template_name_or_list='booking.html', + club=foundClub, + competition=foundCompetition) else: flash("Something went wrong-please try again") - return render_template('welcome.html', club=club, competitions=competitions) + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) @app.route('/purchasePlaces',methods=['POST']) @@ -46,9 +83,39 @@ def purchasePlaces(): 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 + print(placesRequired) + + if placesRequired < 0: + flash("Sorry, you should type a positive number.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error="Negative number"), 403 + + elif placesRequired > 12: + flash("Sorry, you are not allow to purchase more than 12 places for this competition.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error="Places max reached"), 403 + + elif placesRequired > int(club['points']): + flash("Sorry, you do not have enough points to purchase.") + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions, + error="Points not enough"), 403 + + update_club_booked_places(club=club, + places=placesRequired, + competition_name=competition["name"]) + + update_competition_available_places(competition=competition, places=placesRequired) + flash('Great-booking complete!') - return render_template('welcome.html', club=club, competitions=competitions) + return render_template(template_name_or_list='welcome.html', + club=club, + competitions=competitions) # TODO: Add route for points display diff --git a/templates/index.html b/templates/index.html index 926526b7d..56b19b23c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,17 @@

Welcome to the GUDLFT Registration Portal!

+ + {% with messages = get_flashed_messages()%} + {% if messages %} + + {% endif%} + {%endwith%} + Please enter your secretary email to continue:
diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py new file mode 100644 index 000000000..7e30da2a2 --- /dev/null +++ b/unit_tests/conftest.py @@ -0,0 +1,109 @@ +import pytest + +from random import randint + +from server import app, clubs + +@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", + "points":"13" + }, + { + "name":"Iron Temple", + "email": "admin@irontemple.com", + "points":"4" + }, + { "name":"She Lifts", + "email": "kate@shelifts.co.uk", + "points":"12" + } + ] + return the_clubs + +@pytest.fixture +def get_competitions(): + the_competitions = [ + { + "name": "Fall Classic", + "date": "2020-10-22 13:30:00", + "numberOfPlaces": "13" + }, + { + "name": "Spring Festival", + "date": "2020-03-27 10:00:00", + "numberOfPlaces": "25" + } + ] + return the_competitions + +@pytest.fixture +def get_existing_mail(): + data = {"email": "kate@shelifts.co.uk"} + return data + +@pytest.fixture +def get_existing_mail_2(): + data = {"email": "admin@irontemple.com"} + return data + +@pytest.fixture +def get_unexisting_mail(): + data = {"email": "nicolas.marie@unexisting.com"} + 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_consistent_purchasing_data(): + competition = "Spring Festival" + club_name = "She Lifts" + places_to_book = randint(1, 12) + data = {"competition": competition, "club": club_name, "places": str(places_to_book)} + + return data + +@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_data_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_data_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 diff --git a/unit_tests/test_app.py b/unit_tests/test_app.py new file mode 100644 index 000000000..16bdf86ef --- /dev/null +++ b/unit_tests/test_app.py @@ -0,0 +1,285 @@ +from bs4 import BeautifulSoup +from flask import url_for + +import server + +def test_index_status_code_ok(client): + response = client.get('/') + assert response.status_code == 200 + +def test_index_return_welcome(client): + response = client.get('/') + data = response.data.decode('utf-8') + + assert "Welcome to the GUDLFT Registration Portal!" in data + assert "Please enter your secretary email to continue:" in data + assert "Email:" in data + +def test_index_mail_authentication_ok(get_existing_mail, get_clubs, mocker, client): + mocker.patch('server.clubs', get_clubs) + response = client.post('/showSummary', data=get_existing_mail) + assert response.status_code == 200 + +def test_index_mail_authentication_returns_summary(mocker, client, get_existing_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + response = client.post('/showSummary', data=get_existing_mail) + data = 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 + +def test_index_mail_authentication_fail(mocker, client, get_unexisting_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + response = client.post('/showSummary', data=get_unexisting_mail) + assert response.status_code == 404 + assert "Sorry, that email was not found." in response.data.decode('utf-8') + +def test_summary_logout_redirect_status_code_ok(mocker, client, get_existing_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) + logout_response = client.get('/logout') + assert logout_response.status_code == 302 + +def test_summary_logout_redirect_returns_welcome(mocker, client, get_existing_mail, get_clubs): + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) + + 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 to continue:" in data + assert "Email:" in data + +def test_booking_status_code_ok(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_clubs): + + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) + + response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + assert response.status_code == 200 + +def test_booking_return_festival_page_booking(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_clubs): + + mocker.patch('server.clubs', get_clubs) + client.post('/showSummary', data=get_existing_mail) + + response = client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + data = response.data.decode('utf-8') + assert "Spring Festival" in data + assert "Places available: " in data + assert "How many places?" in data + +def test_good_purchasing_places_status_code_ok(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_consistent_purchasing_data, + get_clubs): + + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') + + response = client.post('/purchasePlaces', data=get_consistent_purchasing_data) + + assert response.status_code == 200 + +def test_good_purchasing_places_returns_summary_page(mocker, client, get_existing_mail, get_clubs, + get_consistent_purchasing_data, + get_competitions): + + mocker.patch('server.clubs', get_clubs) + mocker.patch('server.competitions', get_competitions) + + client.post('/showSummary', data=get_existing_mail) + + 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['numberOfPlaces'] + + mocker.patch('server.save_clubs') + mocker.patch('server.save_competitions') + + response = client.post('/purchasePlaces', data=purchasing_data) + data = 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: 2020-03-27 10:00:00\n' + f' Number of Places: {new_competition_places}\n \n' + f' Book Places\n' + f'
  • ') + + assert "Great-booking 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 + +def test_purchasing_places_not_enough_points_status_code_error(mocker, + client, + get_existing_mail_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data, + get_clubs): + + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail_2) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club_2['competition'], + club=get_existing_competition_and_club_2['club'])) + + response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + + assert response.status_code == 403 + +def test_purchasing_places_not_enough_points_returns_sorry(mocker, + client, + get_existing_mail_2, + get_existing_competition_and_club_2, + get_inconsistent_purchasing_data, + get_clubs): + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail_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] + + response = client.post('/purchasePlaces', data=get_inconsistent_purchasing_data) + data = response.data.decode('utf-8') + + assert "Sorry, you do not have enough points to purchase." in data + assert f"Points available: {the_club['points']}" in data + +def test_purchasing_places_over_12_places_status_code_error(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_data_over_12_places, + get_clubs): + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + response = client.post('/purchasePlaces', data=purchasing_data_over_12_places) + + assert response.status_code == 403 + +def test_purchasing_places_over_12_places_returns_sorry(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_data_over_12_places, + get_clubs): + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + purchasing_data = purchasing_data_over_12_places + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + response = client.post('/purchasePlaces', data=purchasing_data) + data = response.data.decode('utf-8') + + 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 + +def test_purchasing_places_negative_number_status_code_error(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + get_clubs, + purchasing_data_with_negative_places): + + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + purchasing_data = purchasing_data_with_negative_places + + response = client.post('/purchasePlaces', data=purchasing_data) + + assert response.status_code == 403 + +def test_purchasing_places_negative_number_returns_sorry(mocker, + client, + get_existing_mail, + get_existing_competition_and_club, + purchasing_data_with_negative_places, + get_clubs): + mocker.patch('server.clubs', get_clubs) + + client.post('/showSummary', data=get_existing_mail) + + client.get(url_for(endpoint='book', + competition=get_existing_competition_and_club['competition'], + club=get_existing_competition_and_club['club'])) + + purchasing_data = purchasing_data_with_negative_places + the_club = [club for club in server.clubs if club["name"] == purchasing_data['club']][0] + club_points = the_club['points'] + + response = client.post('/purchasePlaces', data=purchasing_data) + data = response.data.decode('utf-8') + + assert "Sorry, you should type a positive number." in data + assert f"Points available: {club_points}" in data