From 42dc307243acd0dc836abbb713c2022fdb5b6188 Mon Sep 17 00:00:00 2001 From: scorreia Date: Sun, 21 Jan 2024 15:40:00 +0100 Subject: [PATCH 1/7] Calendar of the competition added --- competitionsApp.py | 11 ++++++++ language/en_US.json | 1 + language/fr_FR.json | 3 ++- language/pl_PL.json | 1 + views/calendar.html | 53 ++++++++++++++++++++++++++++++++++++++ views/skala3ma-layout.html | 8 ++++++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 views/calendar.html diff --git a/competitionsApp.py b/competitionsApp.py index 2e5e919..d2bdd02 100644 --- a/competitionsApp.py +++ b/competitionsApp.py @@ -574,6 +574,17 @@ def privacy(): reference_data=competitionsEngine.reference_data ) +@fsgtapp.route('/calendar') +def calendar(): + return render_template('calendar.html', + competitionName=None, + session=session, + user=competitionsEngine.get_user_by_email(session.get('email')), + reference_data=competitionsEngine.reference_data, + competitions=competitionsEngine.getCompetitions(), + langpack=languages['en_US'], + **session + ) @fsgtapp.route('/main') diff --git a/language/en_US.json b/language/en_US.json index 727835c..d36a20d 100644 --- a/language/en_US.json +++ b/language/en_US.json @@ -30,6 +30,7 @@ "Instant_scoring": "Instant scoring", "Multi_language": "Multi-language", "Google_and_Facebook_logins":"Google and Facebook logins", + "calendar": "Calendar", "number_of_routes": "Number of routes", "address": "Address", diff --git a/language/fr_FR.json b/language/fr_FR.json index bf27379..95f8431 100644 --- a/language/fr_FR.json +++ b/language/fr_FR.json @@ -30,7 +30,8 @@ "Instant_scoring": "Calcul instantanné des scores", "Multi_language": "Multi-langue", "Google_and_Facebook_logins":"Login Google et Facebook", - + "calendar": "Calendrier", + "number_of_routes": "Nombre de voies", "address": "Adresse", "link": "URL", diff --git a/language/pl_PL.json b/language/pl_PL.json index a7c61bc..4dc3157 100644 --- a/language/pl_PL.json +++ b/language/pl_PL.json @@ -30,6 +30,7 @@ "Instant_scoring": "Instant scoring", "Multi_language": "Multi-language", "Google_and_Facebook_logins":"Google and Facebook logins", + "calendar": "Calendar", "competition_climber_list": "Competition climber list", "number_of_routes": "Number of routes", diff --git a/views/calendar.html b/views/calendar.html new file mode 100644 index 0000000..bd138c8 --- /dev/null +++ b/views/calendar.html @@ -0,0 +1,53 @@ +{% extends "skala3ma-layout.html" %} + +{% block topcontent %} + +{% endblock %} + + {% block secondarycontent %} + + +
+
+
+

{{ reference_data['current_language'].calendar}}

+
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/views/skala3ma-layout.html b/views/skala3ma-layout.html index be58039..c290564 100644 --- a/views/skala3ma-layout.html +++ b/views/skala3ma-layout.html @@ -185,6 +185,10 @@ + + + + @@ -224,6 +228,10 @@

+
  • + {{ reference_data['current_language'].calendar}} +
  • +
  • {{ reference_data['current_language'].gyms }}
  • From 6c91e25b7b2adc47abfd45f0d9d552c48c669924 Mon Sep 17 00:00:00 2001 From: David Mossakowski Date: Tue, 23 Jan 2024 18:01:25 +0100 Subject: [PATCH 2/7] David/20231126 updated dependencies a (#32) 49 hours of work - 6 days c0a0f70 translations and better new gym flow 38d0f57 nicer display - 1h a085e92 fix flicker of routes when deleting activity 1h d74705d flow and view changes 4h 6aac200 add activity graph 4h 2939db1 stop calculating the stats for now 45ed92d fix calculation issue 7b80a10 start stats for activities 2h e36ac43 small fix 9e490f6 quick fix 8bb1aad align display tables 1h 1a5fd92 better activity detail ui 4h 8eb840d better style route input in activity 1h 4402f03 fix error; add raw admin c906bc6 add pydantic 1379df5 Activities added 32h ce024ae fix post json issue 680bb0a fix data dir 717788f fix data dir 77396a4 add gevent e56cd3d add gunicorn 755a8ee use cwd for DATA_DIRECTORY --- Activity.py | 24 + skala_journey.py => activities_db.py | 149 +++-- competitionsApp2.py | 383 ----------- competitionsEngine.py | 31 +- language/en_US.json | 17 +- language/fr_FR.json | 18 +- language/pl_PL.json | 17 +- competitionsApp.py => main_app_ui.py | 316 +++++---- public/css/style.css | 54 +- ...1_correct_mark_success_tick_valid_icon.png | Bin 0 -> 4421 bytes ...elictricity_light_lightning_storm_icon.png | Bin 0 -> 6526 bytes ...unded_confused_emoji_emotion_fail_icon.png | Bin 0 -> 16041 bytes requirements.txt | 129 +--- server.py | 21 +- skala_api.py | 297 ++++++++- skala_db.py | 7 +- views/activities.html | 525 +++++++++++++++ views/activity-detail.html | 603 ++++++++++++++++++ views/competitionClimber.html | 2 +- views/competitionRawAdmin.html | 6 +- views/competitionStats.html | 6 +- views/gym-print.html | 2 +- views/gym-routes.html | 50 +- views/gymedit.html | 2 +- views/gyms.html | 4 +- views/myskala.html | 4 +- views/skala-journey.html | 85 ++- views/skala3ma-layout.html | 19 +- 28 files changed, 2049 insertions(+), 722 deletions(-) create mode 100644 Activity.py rename skala_journey.py => activities_db.py (50%) delete mode 100644 competitionsApp2.py rename competitionsApp.py => main_app_ui.py (88%) create mode 100644 public/images/1398911_correct_mark_success_tick_valid_icon.png create mode 100644 public/images/2682840_bolt_elictricity_light_lightning_storm_icon.png create mode 100644 public/images/4125784_confounded_confused_emoji_emotion_fail_icon.png create mode 100644 views/activities.html create mode 100644 views/activity-detail.html diff --git a/Activity.py b/Activity.py new file mode 100644 index 0000000..13f6549 --- /dev/null +++ b/Activity.py @@ -0,0 +1,24 @@ + + +class Activity: + def __init__(self, name, mur, start, end): + self.name = name + self.mur = mur + self.start = start + self.end = end + + + def __str__(self): + return self.name + " " + self.mur + " " + self.start + " " + self.end + + def to_json(): + activity = {"id": activity_id, "gym_id": gym_id, "routes_id": routes_id, "description": description, "routes": [], + } + + activity['id']=activity_id + activity['user_id']=user_id + activity['date']=date + + + + diff --git a/skala_journey.py b/activities_db.py similarity index 50% rename from skala_journey.py rename to activities_db.py index 449743d..3994e3c 100644 --- a/skala_journey.py +++ b/activities_db.py @@ -26,18 +26,21 @@ sql_lock = RLock() DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') + +if DATA_DIRECTORY is None: + DATA_DIRECTORY = os.getcwd() + #PLAYLISTS_DB = DATA_DIRECTORY + "/db/playlists.sqlite" COMPETITIONS_DB = DATA_DIRECTORY + "/db/competitions.sqlite" -JOURNEYS_TABLE = "journeys" -JOURNEY_ENTRY_DB = "journey_entries" +activities_TABLE = "activities" route_finish_status = {0: "attempt", 1: "flash", 2: "redpoint", 3: "toprope"} def init(): - logging.info('initializing skala_journey...') + logging.info('initializing skala_activity...') if os.path.exists(DATA_DIRECTORY) and os.path.exists(COMPETITIONS_DB): db = lite.connect(COMPETITIONS_DB) @@ -45,7 +48,7 @@ def init(): # ptype 0-public cursor = db.cursor() - cursor.execute('''CREATE TABLE if not exists ''' + JOURNEYS_TABLE + '''( + cursor.execute('''CREATE TABLE if not exists ''' + activities_TABLE + '''( id text NOT NULL UNIQUE, user_id text NOT NULL, gym_id text NOT NULL, @@ -55,77 +58,109 @@ def init(): )''') db.commit() - print('created ' + JOURNEYS_TABLE) + print('created ' + activities_TABLE) + -def add_journey_session(user, gym_id, routes_id, date): - journey_id = str(uuid.uuid4().hex) +def add_activity(user, gym, name, date): + activity_id = str(uuid.uuid4().hex) - journey = {"id": journey_id, "gym_id": gym_id, "routes_id": routes_id, "description": "", "routes": [], + gym_id = gym.get('id') + routes_id = gym.get('routesid') + activity = {"id": activity_id, "gym_id": gym_id, "routes_id": routes_id, "starttime": date, "name": name, + "gym_name": gym.get('name'), + "routes": [] } # write this competition to db - _add_journey_session(journey_id, user.get('id'), gym_id, routes_id, date, journey) - return id + _add_activity(activity_id, user.get('id'), gym_id, routes_id, date, activity) + return activity_id -def add_journey_session2(date, user, gym_id, routes_id, description): - journey_id = str(uuid.uuid4().hex) - journey = {"id": journey_id, "gym_id": gym_id, "routes_id": routes_id, "description": description, "routes": [], - } - # write this competition to db - _add_journey_session(date, user, gym_id, routes_id, description, journey) - return id +def get_activity(session_id): + return _get_activity(session_id) -def get_journey_session(session_id): - return _get_journey(session_id) -def get_journey_sessions(user_id): - return _get_journey_sessions_by_user_id(user_id) +def get_activities(user_id): + return _get_activities_by_user_id(user_id) + # add an entry to an existing session -def add_journey_session_entry(journey_id, route_id, status, note): +def add_activity_entry(activity_id, route, status, note): entry_id = str(uuid.uuid4().hex) - journey = get_journey_session(journey_id) + route_id = route.get('id') + + activity = get_activity(activity_id) session_entry = {"id": entry_id, "route_id": route_id, "status": status, "note": note, } - journey.get('routes').append(session_entry) + session_entry = {**route, **session_entry} + activity.get('routes').append(session_entry) + # write this competition to db + _update_activity(activity_id, activity.get("user_id"), activity.get("gym_id"), activity.get("routes_id"), activity); + + return activity + +def update_activity(activity_id, activity_json): + if activity_json is None or activity_id is None: + return None # write this competition to db - _update_journey(journey_id, journey.get("user_id"), journey.get("gym_id"), journey.get("routes_id"), journey); + _update_activity_jsondata(activity_id, activity_json) + + +def delete_activity(activity_id): + activity = get_activity(activity_id) + + if activity is None: + return None + + try: + sql_lock.acquire() + + db = lite.connect(COMPETITIONS_DB) + cursor = db.cursor() + + cursor.execute("delete from " + activities_TABLE + " where id =? ", + [str(activity_id)]) + finally: + db.commit() + db.close() + sql_lock.release() + logging.info("deleted activity for user:"+str(activity_id)) + + return activity - return journey -def remove_journey_session(journey_id, entry_id): - journey = get_journey_session(journey_id) +def delete_activity_route(activity_id, entry_id): + activity = get_activity(activity_id) - if journey is None: + if activity is None: return None - for route_index, route in enumerate(journey['routes']): + for route_index, route in enumerate(activity['routes']): if route['id'] == entry_id: - journey['routes'].pop(int(route_index)) - _update_journey(journey_id, journey.get("user_id"), journey.get("gym_id"), journey.get("routes_id"), journey); + activity['routes'].pop(int(route_index)) + _update_activity(activity_id, activity.get("user_id"), activity.get("gym_id"), activity.get("routes_id"), activity); - return journey + return activity -def _add_journey_session(journey_id, user_id, gym_id, routes_id, date, jsondata): +def _add_activity(activity_id, user_id, gym_id, routes_id, date, jsondata): try: sql_lock.acquire() db = lite.connect(COMPETITIONS_DB) cursor = db.cursor() - jsondata['id']=journey_id + jsondata['id']=activity_id jsondata['user_id']=user_id jsondata['gym_id']=gym_id jsondata['routes_id']=routes_id jsondata['date']=date - cursor.execute("INSERT INTO " + JOURNEYS_TABLE + " (id, user_id, gym_id, routes_id, added_at, jsondata ) " + cursor.execute("INSERT INTO " + activities_TABLE + " (id, user_id, gym_id, routes_id, added_at, jsondata ) " "values (?, ?, ?, ?, ?, ?)", - [str(journey_id), str(user_id), str(gym_id), str(routes_id), date, json.dumps(jsondata)]) + [str(activity_id), str(user_id), str(gym_id), str(routes_id), date, json.dumps(jsondata)]) finally: db.commit() db.close() @@ -133,15 +168,33 @@ def _add_journey_session(journey_id, user_id, gym_id, routes_id, date, jsondata) logging.info("added climbing session for user:"+str(user_id)) -def _get_journey(journey_id): +def _update_activity_jsondata(activity_id, new_jsondata): + try: + sql_lock.acquire() + + db = lite.connect(COMPETITIONS_DB) + cursor = db.cursor() + + # Convert new_jsondata to a JSON string + new_jsondata_str = json.dumps(new_jsondata) + + cursor.execute(f"UPDATE {activities_TABLE} SET jsondata = ? WHERE id = ?", (new_jsondata_str, str(activity_id))) + finally: + db.commit() + db.close() + sql_lock.release() + logging.info(f"Updated jsondata for activity: {activity_id}") + + +def _get_activity(activity_id): try: #sql_lock.acquire() db = lite.connect(COMPETITIONS_DB) cursor = db.cursor() - result = cursor.execute("select jsondata from " + JOURNEYS_TABLE + " where id =? ", - [str(journey_id)]) + result = cursor.execute("select jsondata from " + activities_TABLE + " where id =? ", + [str(activity_id)]) result = result.fetchone() if result is None or result[0] is None: @@ -159,23 +212,23 @@ def _get_journey(journey_id): #logging.info("retrieved climbing session for user:"+str(session_id)) -def _get_journey_sessions_by_user_id(user_id): +def _get_activities_by_user_id(user_id): try: #sql_lock.acquire() db = lite.connect(COMPETITIONS_DB) cursor = db.cursor() - result = cursor.execute("select jsondata from " + JOURNEYS_TABLE + " where user_id =? ", + result = cursor.execute("select jsondata from " + activities_TABLE + " where user_id =? ", [str(user_id)]) #result = result.fetchall() - journeys=[] + activities=[] if result is not None and result.arraysize > 0: for row in result.fetchall(): # comp = row[0] - journeys.append(json.loads(row[0])) + activities.append(json.loads(row[0])) # gyms[gym['id']] = gym - return journeys + return activities finally: db.commit() @@ -187,7 +240,7 @@ def _get_journey_sessions_by_user_id(user_id): -def _update_journey(journey_id, user_id, gym_id, routes_id, jsondata): +def _update_activity(activity_id, user_id, gym_id, routes_id, jsondata): try: sql_lock.acquire() @@ -195,8 +248,8 @@ def _update_journey(journey_id, user_id, gym_id, routes_id, jsondata): cursor = db.cursor() cursor.execute( - "update " + JOURNEYS_TABLE + " set user_id = ?, gym_id = ?, routes_id = ?, jsondata = ? where id=?", - [str(user_id), str(gym_id), str(routes_id), json.dumps(jsondata), str(journey_id)]) + "update " + activities_TABLE + " set user_id = ?, gym_id = ?, routes_id = ?, jsondata = ? where id=?", + [str(user_id), str(gym_id), str(routes_id), json.dumps(jsondata), str(activity_id)]) finally: db.commit() diff --git a/competitionsApp2.py b/competitionsApp2.py deleted file mode 100644 index b876e17..0000000 --- a/competitionsApp2.py +++ /dev/null @@ -1,383 +0,0 @@ -# Copyright (C) 2023 David Mossakowski -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -import json -import os -import glob -import random -from datetime import datetime, date, time, timedelta -import numpy as np -import pandas as pd -import numpy.random -from collections import Counter -import matplotlib.pyplot as plt -import matplotlib.cm as cm -import plotly -import plotly.graph_objects as go -import plotly.express as px -import tracemalloc -import sqlite3 as lite -import uuid -import competitionsEngine -import traceback - -from flask import Flask, redirect, url_for, session, request, render_template, send_file, jsonify, Response, \ - stream_with_context, copy_current_request_context - -from sklearn.cluster import KMeans - -from collections import defaultdict - -from matplotlib.figure import Figure -from sklearn.preprocessing import MinMaxScaler - -from functools import lru_cache -import logging -from dotenv import load_dotenv - -from flask import Blueprint - -fsgtapp = Blueprint('fsgtapp', __name__) - -from authlib.integrations.flask_client import OAuth -from authlib.integrations.flask_client import OAuthError - - - - -load_dotenv() - -DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') -#PLAYLISTS_DB = DATA_DIRECTORY + "/db/playlists.sqlite" -COMPETITIONS_DB = DATA_DIRECTORY + "/db/competitions.sqlite" - -# ID, date, name, location -COMPETITIONS_TABLE = "competitions" -# ID, name, club, m/f, list of climbs -CLIMBERS_TABLE = "climbers" - -FSGT_APP_ID = os.getenv('FSGT_APP_ID') -FSGT_APP_SECRET = os.getenv('FSGT_APP_SECRET') -DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') -GOOGLE_DISCOVERY_URL = ( - "https://accounts.google.com/.well-known/openid-configuration" -) -fsgtapp.debug = True -fsgtapp.secret_key = 'development' -oauth = OAuth(fsgtapp) - -genres = {"test": "1"} -authenticated = False - - -comps = {} -climbers = {} - - -def fetch_token(): - #log.info('fetch_token') - logging.info ("fetching token... ") - return session.get('token') - - -def update_token(name, token, refresh_token=None, access_token=None): - #log.info('update_token') - logging.info ("updating token... ") - session['token'] = token - return session['token'] - - -googleAuth = oauth.register( - name='google', - client_id=FSGT_APP_ID, - client_secret=FSGT_APP_SECRET, - #consumer_key=SPOTIFY_APP_ID, - #consumer_secret=SPOTIFY_APP_SECRET, - # Change the scope to match whatever it us you need - # list of scopes can be found in the url below - access_token_params=None, - #scope = 'playlist-read-private user-library-read user-top-read', - base_url='https://accounts.spotify.com/', - #request_token_url='https://accounts.spotify.com/api/token', - access_token_url='https://accounts.spotify.com/api/token', - refresh_token_url='https://accounts.spotify.com/api/token', - authorize_url='https://accounts.spotify.com/authorize', - fetch_token=fetch_token, - update_token=update_token, - client_kwargs = { - 'scope': 'user-library-read user-top-read playlist-read-private playlist-read-collaborative' - #'scope': 'playlist-read-private user-library-read user-top-read' - } -) - - - - -@app.route('/fsgtlogin/authorized') -def spotify_authorized(): - logging.info("spotify is calling /login/authorized ...") - try: - error = request.args.get('error') - logging.info(str(request)) - - if str(error) == 'access_denied': - logging.info ("access is denied ",error) - return render_template('index.html', subheader_message="Not authorized", library={}, **session) - - #acc = spotify.fetch_access_token(scope='user-library-read') - resp = googleAuth.authorize_access_token() - logging.info ("spotify calls us now " + str(resp)) - if resp is None: - return 'Access denied: reason={0} error={1}'.format( - request.args['error_reason'], - request.args['error_description'] - ) - #if isinstance(resp, OAuthException): - # return 'Access denied: {0}'.format(resp.message) - - session['token'] = resp - session['access_token'] = (resp['access_token'], '') - session['refresh_token'] = (resp['refresh_token'], '') - session['expires_at'] = resp['expires_at'] - session['expires_in'] = resp['expires_in'] - session['expires_at_localtime'] = int(datetime.datetime.now().timestamp()+int(resp['expires_in'])-1000) - session.dataLoadingProgressMsg = '' - - # - # me = spotify.get('/v1/me') - #getPlaylists(session['oauth_token'], 'dmossakowski') - - global authenticated - authenticated = True - #print me.data - - - - if session.get("wants_url") is not None: - return redirect(session["wants_url"]) - else: - return redirect("/") - - except OAuthError as e: - logging.info(" error in authentication ", traceback.format_exc()) - return render_template('index.html', - subheader_message="Authentication error "+str(traceback.format_exc()), - library={}, - **session) - - - - -@fsgtapp.route('/login') -def login(): - logging.info ("doing login "+str(request.referrer)+" client_id "+str(SPOTIFY_APP_ID)) - - callback = url_for('spotify_authorized', _external=True) - - #print(" request.referrer " + request.referrer) - #print(" request.args.get('next') ="+request.args.get('next')) - - callback2 = url_for( - 'spotify_authorized', - next=request.args.get('next') or request.referrer or None, - _external=True - ) - return spotify.authorize_redirect(callback) - - -@fsgtapp.route('/competitionDashboard') -#@login_required -def getCompetitionDashboard(): - - username = request.args.get('username') - name = request.args.get('name') - date = request.args.get('date') - gym = request.args.get('gym') - comp = {} - competitionId=None - - - if name is not None and date is not None and gym is not None: - - competitionId = competitionsEngine.addCompetition(None, name, date, gym) - comp = getCompetition(competitionId) - return redirect(url_for('getCompetition', competitionId=competitionId)) - - subheader_message='Create new competition ' - competitions= competitionsEngine.getCompetitions() - - return render_template('competitionDashboard.html', - subheader_message=subheader_message, - competitions=competitions, - competitionName=None, - **session) - - - - - -@fsgtapp.route('/competitionDashboard//register') -def addCompetitionClimber(competitionId): - - username = request.args.get('username') - name = request.args.get('name') - sex = request.args.get('sex') - club = request.args.get('club') - - comp = competitionsEngine.getCompetition(competitionId) - subheader_message = 'Please register for ' + comp['name'] + ' on ' + comp['date'] - - climber=None - - - if name is not None and sex is not None and club is not None: - climber = competitionsEngine.addClimber(None, competitionId, name, club, sex) - subheader_message = 'You have been registered! Thanks!' - else: - comp=None # this is to not show the list of climbers before registration - - competitions= competitionsEngine.getCompetitions() - - return render_template('competitionClimber.html', - subheader_message=subheader_message, - competition=comp, - competitionId=competitionId, - climber=climber, - **session) - - - - -@fsgtapp.route('/competitionDashboard/') -#@login_required -def getCompetition(competitionId): - #competitionId = request.args.get('competitionId') - - - # logging.info(session['id']+' competitionId '+competitionId) - # r = request - # username = request.args.get('username') - - competition = None - - if competitionId is not None: - competitionsEngine.recalculate(competitionId) - competition = competitionsEngine.getCompetition(competitionId) - - if competition is None: - return render_template('competitionDashboard.html', sortedA=None, - subheader_message="No competition found", - **session) - elif competition is LookupError: - return render_template('index.html', sortedA=None, - getPlaylistError="Playlist was not found", - library={}, - **session) - elif len(competition) == 0: - - return render_template('index.html', sortedA=None, - getPlaylistError="Playlist has no tracks or it was not found", - library={}, - **session) - - - subheader_message = "Competition '" + competition['name'] + "' on "+competition['date'] - - # library= {} - # library['tracks'] = tracks - # playlist = json.dumps(playlist) - # u = url_for('getRandomPlaylist', playlistName=playlistName, playlist=playlist, - # subheader_message=subheader_message) - # return redirect(url_for('getRandomPlaylist', playlistName=playlistName, playlist=playlist, - # subheader_message=subheader_message, - # library=None, - # **session)) - - return render_template("competitionDashboard.html", competitionId=competitionId, competition=competition, - subheader_message=subheader_message, - library=None, - **session) - - - - -@fsgtapp.route('/competitionDashboard//climber/') -#@login_required -def getCompetitionClimber(competitionId, climberId): - #competitionId = request.args.get('competitionId') - - routesUpdated = [] - for i in range(100): - routeChecked = request.args.get("route"+str(i)) != None - if routeChecked: routesUpdated.append(i) - - - # logging.info(session['id']+' competitionId '+competitionId) - # r = request - # username = request.args.get('username') - - competition = None - - if climberId is not None: - if len(routesUpdated) > 0: - competitionsEngine.setRoutesClimbed(competitionId, climberId, routesUpdated) - competition = competitionsEngine.getCompetition(competitionId) - return render_template('competitionDashboard.html', sortedA=None, - competition=competition, - competitionId=competitionId, - subheader_message="Climber routes saved", - **session) - - - climber = competitionsEngine.getClimber(competitionId,climberId) - - - if climber is None: - return render_template('competitionDashboard.html', sortedA=None, - subheader_message="No climber found", - **session) - elif climber is LookupError: - return render_template('index.html', sortedA=None, - getPlaylistError="error ", - library={}, - **session) - elif len(climber) == 0: - return render_template('index.html', sortedA=None, - getPlaylistError="Playlist has no tracks or it was not found", - library={}, - **session) - - competition = competitionsEngine.getCompetition(competitionId) - subheader_message = climber['name']+" from "+climber['club'] - - # library= {} - # library['tracks'] = tracks - # playlist = json.dumps(playlist) - # u = url_for('getRandomPlaylist', playlistName=playlistName, playlist=playlist, - # subheader_message=subheader_message) - # return redirect(url_for('getRandomPlaylist', playlistName=playlistName, playlist=playlist, - # subheader_message=subheader_message, - # library=None, - # **session)) - - return render_template("competitionDashboard.html", climberId=climberId, climber=climber, - subheader_message=subheader_message, - competitionId=competitionId, - **session) - - - - diff --git a/competitionsEngine.py b/competitionsEngine.py index d55d282..8f85e19 100644 --- a/competitionsEngine.py +++ b/competitionsEngine.py @@ -20,8 +20,6 @@ import random from datetime import datetime, date, timedelta import time -import numpy as np -import numpy.random from collections import Counter import tracemalloc import sqlite3 as lite @@ -33,7 +31,7 @@ import requests import skala_db -import skala_journey +import activities_db sql_lock = RLock() from flask import Flask, redirect, url_for, session, request, render_template, send_file, jsonify, Response, \ @@ -48,6 +46,10 @@ load_dotenv() DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') + +if DATA_DIRECTORY is None: + DATA_DIRECTORY = os.getcwd() + #PLAYLISTS_DB = DATA_DIRECTORY + "/db/playlists.sqlite" COMPETITIONS_DB = DATA_DIRECTORY + "/db/competitions.sqlite" @@ -145,10 +147,10 @@ reference_data = {"categories":categories, "categories_ado":categories_ado, "clubs":clubs, "competition_status": competition_status, "colors_fr":colors, - "supported_languages":supported_languages, "route_finish_status": skala_journey.route_finish_status, + "supported_languages":supported_languages, "route_finish_status": activities_db.route_finish_status, "competition_types":competition_types} -# called from competitionsApp +# called from main_app_ui def addCompetition(compId, name, date, routesid, max_participants, competition_type): if compId is None: compId = str(uuid.uuid4().hex) @@ -699,7 +701,7 @@ def init(): # "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=10224632176365169&height=50&width=50&ext=1648837065&hash=AeTqQus7FdgHfkpseKk") - skala_journey.init() + activities_db.init() print('created ' + COMPETITIONS_DB) @@ -1201,6 +1203,17 @@ def get_routes(routesid): return routes +def get_route(routesid, route_id): + routes = get_routes(routesid) + if routes is None: + return None + routes = routes['routes'] + for route in routes: + if route['id'] == route_id: + return route + return None + + def get_routes_by_gym_id(gym_id): return skala_db.get_routes_by_gym_id(gym_id) @@ -1382,13 +1395,13 @@ def generate_dummy_routes(size): routes_id = str(uuid.uuid4().hex) routes = {"id":routes_id } routesA = [] - for i in range(1, size): + for i in range(1, size+1): route_id = str(uuid.uuid4().hex) - route = _get_route_dict(route_id, str(i), '1', '#2E8857', 'solid', '-', '', '', '', '') + route = _get_route_dict(route_id, str(i), '1', '#2E8857', 'solid', '-', 'route'+str(i), '', '', '') routesA.append(route) routes['routes'] = routesA - routes['name'] = "Dummy routes" + routes['name'] = "Default routes" return routes diff --git a/language/en_US.json b/language/en_US.json index d36a20d..8f56d37 100644 --- a/language/en_US.json +++ b/language/en_US.json @@ -144,12 +144,27 @@ "ado_contest_rules": "Ado contest rules", "contact_email": "Contact organizers", + "activities": "Activities", + "add_activity": "Add activity", + "delete_activity": "Delete activity", + "activity": "Activity", + "activity_name": "Activity name", + "activity_date": "Activity date", + "activity_time": "Activity time", + "activity_location": "Activity location", + "activity_description": "Activity description", + "attempted": "Attempted", + "flashed": "Flashed", + + "error5314": "Updating routes is not permitted for this user", "error5315": "No permission to edit gym", "error5316": "User with this email is known and they should login and register themselves", "error5321": "This email address is already registered", "error5322": "Current competition status does not allow new registrations", - "error5323": "Maximum registrations reached" + "error5323": "Maximum registrations reached", + "error5324": "Choose a valid gym", + "last": "Last" } diff --git a/language/fr_FR.json b/language/fr_FR.json index 95f8431..34c12a8 100644 --- a/language/fr_FR.json +++ b/language/fr_FR.json @@ -70,7 +70,7 @@ "marble": "marbrée", "route_num": "#", "grade": "Cotation", - "climbed": "enchaînée", + "climbed": "Enchaînée", "route_name": "Nom", "route_opened_by": "Ouverte par", "route_opened_date": "Date d'ouverture", @@ -148,12 +148,26 @@ "ado_contest_rules": "Règlement Ado", "contact_email": "Contacter les organisateurs", + "activities": "Activités", + "add_activity": "Nouvelle activité", + "delete_activity": "Supprimer activité", + "activity": "Activité", + "activity_name": "Activity name", + "activity_date": "Activity date", + "activity_time": "Activity time", + "activity_location": "Activity location", + "activity_description": "Activity description", + "attempted": "Essayé", + "flashed": "Flashé", + "error5314": "La mise à jour des voies n'est pas autorisée", "error5315": "Pas de permission pour éditer le mur", "error5316": "Cet email est connu. L'utilisateur doit se connecter et s'inscrire lui-même", "error5321": "Un utilisateur avec cet email est déjà inscrit", "error5322": "L'état de la compétition ne permet pas de nouvelles inscriptions", - "error5323": "Nombre maximal d'inscriptions atteint" + "error5323": "Nombre maximal d'inscriptions atteint", + "error5324": "Choose a valid gym", + "last": "Last" } diff --git a/language/pl_PL.json b/language/pl_PL.json index 4dc3157..83fa360 100644 --- a/language/pl_PL.json +++ b/language/pl_PL.json @@ -150,13 +150,26 @@ "ado_contest_rules": "Ado contest rules", "contact_email": "Contact organizers", + "activities": "Wyczyny", + "add_activity": "Dodaj wyczyn", + "delete_activity": "Wymaż wyczyn", + "activity": "Wyczyn", + "activity_name": "Nazwa wyczynu", + "activity_date": "Data wyczynu", + "activity_time": "Czas wyczynu", + "activity_location": "Lokalizacja wyczynu", + "activity_description": "Opis wyczynu", + "attempted": "Spróbowane", + "flashed": "Flashed", + "error5314": "Ten użytkownik nie moze zmieniać dróg", "error5315": "Ten użytkownik nie moze zmieniać skałe", "error5316": "Ten użytkownik jest znany i powinien sie wpisac sam i zapisac sam", "error5321": "Ten adres jest juz zarejestrowany", "error5322": "Rejestracja na te zawody jest niemożliwa", - "error5323": "Osiągnięto maksymalną liczbę zawodników" - + "error5323": "Osiągnięto maksymalną liczbę zawodników", + "error5324": "Choose a valid gym", + "last": "Last" } diff --git a/competitionsApp.py b/main_app_ui.py similarity index 88% rename from competitionsApp.py rename to main_app_ui.py index d2bdd02..96e9837 100644 --- a/competitionsApp.py +++ b/main_app_ui.py @@ -23,20 +23,21 @@ import competitionsEngine import csv from functools import wraps +import qrcode from flask import Flask, redirect, url_for, session, request, render_template, send_file, send_from_directory, jsonify, Response, \ - stream_with_context, copy_current_request_context, g + stream_with_context, copy_current_request_context, make_response import logging from dotenv import load_dotenv from flask import Blueprint -import skala_journey as journeys_engine +import activities_db as activity_engine from io import BytesIO from flask import send_file - +import skala_api # Third party libraries from flask import Flask, redirect, request, url_for @@ -50,16 +51,24 @@ from oauthlib.oauth2 import WebApplicationClient import requests -#fsgtapp = Blueprint('fsgtapp', __name__) +#app_ui = Blueprint('app_ui', __name__) from authlib.integrations.flask_client import OAuth from authlib.integrations.flask_client import OAuthError +import activities_db as activities_db +#from flask_openapi3 import Info, Tag, APIBlueprint +#from flask_openapi3 import OpenAPI + languages = {} load_dotenv() DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') + +if DATA_DIRECTORY is None: + DATA_DIRECTORY = os.getcwd() + #PLAYLISTS_DB = DATA_DIRECTORY + "/db/playlists.sqlite" COMPETITIONS_DB = DATA_DIRECTORY + "/db/competitions.sqlite" @@ -70,16 +79,18 @@ FSGT_APP_ID = os.getenv('FSGT_APP_ID') FSGT_APP_SECRET = os.getenv('FSGT_APP_SECRET') -DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') + GOOGLE_DISCOVERY_URL = ( "https://accounts.google.com/.well-known/openid-configuration" ) -fsgtapp = Blueprint('fsgtapp', __name__) +#tag = Tag(name="UI operations", description='UI operations which return HTML pages - competitionsApp.py') +#app_ui = APIBlueprint('app_ui', __name__, abp_tags=[tag]) +app_ui = Blueprint('app_ui', __name__) -fsgtapp.debug = True -fsgtapp.secret_key = 'development' -oauth = OAuth(fsgtapp) +app_ui.debug = True +app_ui.secret_key = 'development' +oauth = OAuth(app_ui) genres = {"test": "1"} authenticated = False @@ -107,12 +118,13 @@ -fsgtapp.secret_key = os.environ.get("SECRET_KEY") or os.urandom(24) +app_ui.secret_key = os.environ.get("SECRET_KEY") or os.urandom(24) UPLOAD_FOLDER = os.path.join(DATA_DIRECTORY,'uploads') + ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) -#fsgtapp.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +#app_ui.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # User session management setup @@ -120,7 +132,7 @@ -@fsgtapp.before_request +@app_ui.before_request def x(*args, **kwargs): if not session.get('language'): #kk = competitionsEngine.supported_languages.keys() @@ -129,7 +141,7 @@ def x(*args, **kwargs): ##return redirect('/en' + request.full_path) -@fsgtapp.route('/language/') +@app_ui.route('/language/') def set_language(language=None): session['language'] = language @@ -157,12 +169,12 @@ def decorated_function(*args, **kwargs): if session['authsource'] == 'google': return redirect(url_for("googleauth")) - #return redirect(url_for("fsgtapp.fsgtlogin")) + #return redirect(url_for("app_ui.fsgtlogin")) else: return fn(*args, **kwargs) else: session["wants_url"] = request.url - return redirect(url_for("fsgtapp.fsgtlogin")) + return redirect(url_for("app_ui.fsgtlogin")) return decorated_function @@ -178,7 +190,7 @@ def decorated_function(*args, **kwargs): return fn(*args, **kwargs) else: session["wants_url"] = request.url - return redirect(url_for("fsgtapp.fsgtlogin")) + return redirect(url_for("app_ui.fsgtlogin")) return decorated_function @@ -193,11 +205,11 @@ def decorated_function(*args, **kwargs): return fn(*args, **kwargs) else: session["wants_url"] = request.url - return redirect(url_for("fsgtapp.fsgtlogin")) + return redirect(url_for("app_ui.fsgtlogin")) return decorated_function -@fsgtapp.route("/aa") +@app_ui.get("/aa") def index(): if session.get('username'): return 'logged in '+session.get('username') @@ -218,10 +230,13 @@ def index(): -@fsgtapp.route('/competitionRawAdmin', methods=['GET']) +@app_ui.route('/competitionRawAdmin', methods=['GET']) @login_required @admin_required def fsgtadminget(): + """ + Load Admin page from competitionRawAdmin.html + """ edittype = request.args.get('edittype') id = request.args.get('id') action = request.args.get('action') @@ -234,11 +249,16 @@ def fsgtadminget(): id=id) -@fsgtapp.route('/competitionRawAdmin', methods=['POST']) +@app_ui.post('/competitionRawAdmin') @login_required @admin_required def fsgtadmin(): + """ + Do admin action + + This is an admin action + """ edittype = request.form.get('edittype') id = request.form.get('id') action = request.form.get('action') @@ -286,7 +306,7 @@ def fsgtadmin(): #jsonobject = {"success": "competition updated"} # None is gymid but this is ok as the routes id will be found competitionsEngine.upsert_routes(id, None, jsonobject) - + if id is not None and action == 'find': jsonobject = competitionsEngine.get_routes(id) @@ -295,6 +315,19 @@ def fsgtadmin(): jsonobject = competitionsEngine.get_all_routes_ids() + elif edittype == 'activities': + if jsonobject is not None and action == 'update': + #jsonobject = {"success": "competition updated"} + # None is gymid but this is ok as the routes id will be found + # competitionsEngine.upsert_routes(id, None, jsonobject) + return + + if id is not None and action == 'find': + jsonobject = activities_db.get_activities(id) + + if id is not None and action == 'findall': + jsonobject = activities_db.get_activities(id) + else : jsonobject = {"error": "choose edit type" } @@ -304,7 +337,9 @@ def fsgtadmin(): id=id) -@fsgtapp.route('/competition_admin/', methods=['GET']) +@app_ui.route('/competition_admin/', methods=['GET']) +#, summary='returns competitionAdmin.html', + # responses={"default": {"description": "Render template competitionAdmin.html"}}) @login_required def competition_admin_get(competition_id): user = competitionsEngine.get_user_by_email(session['email']) @@ -313,7 +348,7 @@ def competition_admin_get(competition_id): user_list = competitionsEngine.get_all_user_emails() if user is None or competition is None or not competitionsEngine.can_edit_competition(user,competition): session["wants_url"] = request.url - return redirect(url_for('fsgtapp.getCompetition', competitionId=competition['id'])) + return redirect(url_for('app_ui.getCompetition', competitionId=competition['id'])) all_routes = competitionsEngine.get_routes_by_gym_id(competition['gym_id']) @@ -327,7 +362,7 @@ def competition_admin_get(competition_id): id=id) -@fsgtapp.route('/competition_admin/', methods=['POST']) +@app_ui.route('/competition_admin/', methods=['POST']) @login_required def competition_admin_post(competition_id): remove_climber = request.form.get('remove_climber') @@ -358,7 +393,7 @@ def competition_admin_post(competition_id): if user is None or competition is None or not competitionsEngine.can_edit_competition(user,competition): session["wants_url"] = request.url - return redirect(url_for("fsgtapp.fsgtlogin")) + return redirect(url_for("app_ui.fsgtlogin")) @@ -439,7 +474,7 @@ def competition_admin_post(competition_id): id=id) -@fsgtapp.route('/fsgtadmin/') +@app_ui.route('/fsgtadmin/') def fsgtadminedit(edittype): j = request.args.get('jsondata') @@ -450,7 +485,7 @@ def fsgtadminedit(edittype): reference_data=competitionsEngine.reference_data) -@fsgtapp.route('/loginchoice') +@app_ui.route('/loginchoice') def fsgtlogin(): return render_template('competitionLogin.html', reference_data=competitionsEngine.reference_data @@ -459,28 +494,47 @@ def fsgtlogin(): -@fsgtapp.route('/journey', methods=['GET']) + + +@app_ui.route('/activities', methods=['GET']) @login_required -def journey_list(): +def activities(): user = competitionsEngine.get_user_by_email(session['email']) - journey = None - journey_id = user.get('journey_id') + return render_template('activities.html', + user=user, + reference_data=competitionsEngine.reference_data, + today=date.today() + ) - #if journey_id is None: - # journey = journeys_engine.add_journey(user, description) - journeys = journeys_engine._get_journey_sessions_by_user_id(user.get('id')) +#@app_ui.route('/activities/', methods=['GET'], description='returns activity-detail.html') +@app_ui.route('/activities/', methods=['GET']) +#@login_required +def activity_detail(activity_id): + user = competitionsEngine.get_user_by_email(session['email']) + activity = activity_engine.get_activity(activity_id) + gym = competitionsEngine.get_gym(activity['gym_id']) - return render_template('skala-journey.html', + #gym = competitionsEngine.get_gym(journey['gym_id']) + + if activity.get('routes_id') is None: + routes = competitionsEngine.get_routes("7134a8ef-fa2e-4672-a247-115773183bcd") # should return Nanterre routes + else: + routes = competitionsEngine.get_routes(activity['routes_id']) + + return render_template('activity-detail.html', user=user, - journeys=journeys, - journey_id=journey_id, reference_data=competitionsEngine.reference_data, - today=date.today() + activity=activity, + routes=routes, + gym=gym, + today=date.today(), + activity_id=activity_id, ) -@fsgtapp.route('/journey/add', methods=['POST']) + +@app_ui.route('/journey/add', methods=['POST']) @login_required def journey_add(): user = competitionsEngine.get_user_by_email(session['email']) @@ -490,13 +544,13 @@ def journey_add(): comp = {} gym = competitionsEngine.get_gym(gym_id) - journey = journeys_engine.add_journey_session(user,gym_id, gym.get('routesid'), date) + journey = activity_engine.add_journey_session(user,gym_id, gym.get('routesid'), date) #journey_id = user.get('journey_id') #if journey_id is None: - # journey = journeys_engine.add_journey(user, description) + # journey = activity_engine.add_journey(user, description) - journeys = journeys_engine._get_journey_sessions_by_user_id(user.get('id')) + journeys = activity_engine._get_journey_sessions_by_user_id(user.get('id')) return render_template('skala-journey.html', user=user, journeys=journeys, @@ -505,11 +559,11 @@ def journey_add(): ) -@fsgtapp.route('/journey/', methods=['GET']) +@app_ui.route('/journey/', methods=['GET']) @login_required def journey_session(journey_id): user = competitionsEngine.get_user_by_email(session['email']) - journey = journeys_engine.get_journey_session(journey_id) + journey = activity_engine.get_journey_session(journey_id) #gym = competitionsEngine.get_gym(journey['gym_id']) @@ -526,7 +580,7 @@ def journey_session(journey_id): ) -@fsgtapp.route('/journey//add', methods=['POST']) +@app_ui.route('/journey//add', methods=['POST']) @login_required def journey_session_entry_add(journey_id): user = competitionsEngine.get_user_by_email(session['email']) @@ -537,9 +591,9 @@ def journey_session_entry_add(journey_id): comp = {} - journey = journeys_engine.get_journey_session(journey_id) + journey = activity_engine.get_journey_session(journey_id) - journey = journeys_engine.add_journey_session_entry(journey_id,route_id, route_finish_status, notes) + journey = activity_engine.add_journey_session_entry(journey_id,route_id, route_finish_status, notes) routes = competitionsEngine.get_routes(journey.get('routes_id')) return render_template('skala-journey-session.html', @@ -550,13 +604,13 @@ def journey_session_entry_add(journey_id): ) -@fsgtapp.route('/journey///remove', methods=['GET']) +@app_ui.route('/journey///remove', methods=['GET']) @login_required def journey_session_remove(journey_id, route_id): user = competitionsEngine.get_user_by_email(session['email']) - journey = journeys_engine.get_journey_session(journey_id) + journey = activity_engine.get_journey_session(journey_id) - journeys_engine.remove_journey_session(journey_id, route_id) + activity_engine.remove_journey_session(journey_id, route_id) routes = competitionsEngine.get_routes(journey.get('routes_id')) return render_template('skala-journey-session.html', @@ -568,7 +622,7 @@ def journey_session_remove(journey_id, route_id): -@fsgtapp.route('/privacy') +@app_ui.route('/privacy') def privacy(): return render_template('privacy.html', reference_data=competitionsEngine.reference_data @@ -587,7 +641,7 @@ def calendar(): ) -@fsgtapp.route('/main') +@app_ui.route('/main') def main(): langs = competitionsEngine.reference_data['languages'] @@ -605,7 +659,7 @@ def main(): ) -@fsgtapp.route('/competitionDashboard') +@app_ui.route('/competitionDashboard') def getCompetitionDashboard(): # select season year depending on current month; # e.g. if 2023-10-01 then season is 2023-24 @@ -616,7 +670,7 @@ def getCompetitionDashboard(): return competitions_by_year(str(season)) -@fsgtapp.route('/competitions/year/') +@app_ui.route('/competitions/year/') def competitions_by_year(year): username = session.get('username') @@ -645,7 +699,7 @@ def competitions_by_year(year): ) -@fsgtapp.route('/newCompetition', methods=['GET']) +@app_ui.route('/newCompetition', methods=['GET']) @login_required def new_competition(): @@ -683,7 +737,7 @@ def new_competition(): **session) -@fsgtapp.route('/newCompetition', methods=['POST']) +@app_ui.route('/newCompetition', methods=['POST']) @login_required def new_competition_post(): username = session.get('username') @@ -703,14 +757,14 @@ def new_competition_post(): user = competitionsEngine.get_user_by_email(session.get('email')) if user is None or not competitionsEngine.can_create_competition(user): - return redirect(url_for('fsgtapp.fsgtlogin', competitionId=competitionId)) + return redirect(url_for('app_ui.fsgtlogin', competitionId=competitionId)) if name is not None and date is not None and routesid is not None and max_participants is not None: competitionId = competitionsEngine.addCompetition(None, name, date, routesid, max_participants, competition_type=competition_type) competitionsEngine.modify_user_permissions_to_competition(user, competitionId, "ADD") comp = getCompetition(competitionId) - return redirect(url_for('fsgtapp.getCompetition', competitionId=competitionId)) + return redirect(url_for('app_ui.getCompetition', competitionId=competitionId)) subheader_message='Welcome ' competitions= competitionsEngine.getCompetitions() @@ -727,7 +781,7 @@ def new_competition_post(): -@fsgtapp.route('/competitionDashboard//register') +@app_ui.route('/competitionDashboard//register') #@login_required def addCompetitionClimber(competitionId): useremail = session.get('email') @@ -766,7 +820,7 @@ def addCompetitionClimber(competitionId): comp = competitionsEngine.getCompetition(competitionId) competitionName = comp['name'] #subheader_message = 'You have been registered! Thanks!' - #return redirect(url_for('fsgtapp.getCompetition', competitionId=competitionId)) + #return redirect(url_for('app_ui.getCompetition', competitionId=competitionId)) return render_template("competitionClimberRegistered.html", competitionId=competitionId, competition=comp, @@ -800,7 +854,7 @@ def addCompetitionClimber(competitionId): **session) -@fsgtapp.route('/user') +@app_ui.route('/user') def get_user(): if session.get('email') is None: return render_template('competitionDashboard.html', sortedA=None, @@ -828,7 +882,7 @@ def get_user(): **session) -@fsgtapp.route('/updateuser') +@app_ui.route('/updateuser') def update_user(): if session.get('email') is None: return render_template('competitionDashboard.html', sortedA=None, @@ -884,7 +938,7 @@ def update_user(): -@fsgtapp.route('/myresultats') +@app_ui.route('/myresultats') @login_required def myskala(): subheader_message = "My skala" @@ -902,7 +956,7 @@ def myskala(): -@fsgtapp.route('/competitionDetails/') +@app_ui.route('/competitionDetails/') #@login_required def getCompetition(competitionId): #competitionId = request.args.get('competitionId') @@ -957,7 +1011,7 @@ def getCompetition(competitionId): -@fsgtapp.route('/competitionResults/') +@app_ui.route('/competitionResults/') #@login_required def getCompetitionResults(competitionId): competition = None @@ -995,7 +1049,7 @@ def getCompetitionResults(competitionId): # Statistics for a competition -@fsgtapp.route('/competitionStats/') +@app_ui.route('/competitionStats/') #@login_required def getCompetitionStats(competitionId): competition = None @@ -1029,7 +1083,7 @@ def getCompetitionStats(competitionId): -#@fsgtapp.route('/competitionDashboard//climber/') +#@app_ui.route('/competitionDashboard//climber/') #@login_required # THIS IS NOT USED PROBABLY!!!! def getCompetitionClimber(competitionId, climberId): @@ -1099,7 +1153,7 @@ def getCompetitionClimber(competitionId, climberId): -@fsgtapp.route('/competitionResults//download') +@app_ui.route('/competitionResults//download') def downloadCompetitionCsv(competitionId): competition = competitionsEngine.getCompetition(competitionId) @@ -1182,7 +1236,7 @@ def flatten(x, name=''): -@fsgtapp.route('/competitionRoutesEntry/') +@app_ui.route('/competitionRoutesEntry/') @login_required def competitionRoutesList(competitionId): #competitionId = request.args.get('competitionId') @@ -1224,7 +1278,7 @@ def competitionRoutesList(competitionId): # enter competition climbed routes for a climber and save them -@fsgtapp.route('/competitionRoutesEntry//climber/', methods=['GET']) +@app_ui.route('/competitionRoutesEntry//climber/', methods=['GET']) @login_required def routes_climbed(competitionId, climberId): @@ -1250,13 +1304,17 @@ def routes_climbed(competitionId, climberId): competition = competitionsEngine.getCompetition(competitionId) if not competitionsEngine.can_update_routes(user,competition): - return redirect(url_for('fsgtapp.competitionRoutesList', competitionId=competitionId)) + return redirect(url_for('app_ui.competitionRoutesList', competitionId=competitionId)) routesid = competition.get('routesid') routes = competitionsEngine.get_routes(routesid) + + climber_name = climber.get('name') + climber_club = climber.get('club') + #routes = routes['routes'] - subheader_message = climber['name']+" - "+climber['club'] + subheader_message = str(climber_name)+" - "+str(climber_club) return render_template("competitionRoutesEntry.html", climberId=climberId, climber=climber, @@ -1268,7 +1326,7 @@ def routes_climbed(competitionId, climberId): **session) -@fsgtapp.route('/competitionRoutesEntry//climber/', methods=['POST']) +@app_ui.route('/competitionRoutesEntry//climber/', methods=['POST']) @login_required def update_routes_climbed(competitionId, climberId): # generate array of marked routes from HTTP request @@ -1329,7 +1387,7 @@ def update_routes_climbed(competitionId, climberId): -@fsgtapp.route('/migrategyms') +@app_ui.route('/migrategyms') def migrategyms(): gyms = competitionsEngine.get_gyms() @@ -1338,10 +1396,13 @@ def migrategyms(): nanterre['homepage'] = 'https://www.esnanterre.com/' competitionsEngine.update_gym("1", "667", json.dumps(nanterre)) - return redirect(url_for('fsgtapp.gyms')) + return redirect(url_for('app_ui.gyms')) + + + ######## GYMS -@fsgtapp.route('/gyms') +@app_ui.route('/gyms') def gyms(): fullname = request.args.get('fullname') nick = request.args.get('nick') @@ -1373,7 +1434,7 @@ def gyms(): -@fsgtapp.route('/gyms/') +@app_ui.route('/gyms/') def gym_by_id(gymid): gym = competitionsEngine.get_gym(gymid) #gym['routesid']='abc1' @@ -1387,9 +1448,11 @@ def gym_by_id(gymid): -@fsgtapp.route('/gyms//', methods=['GET']) +@app_ui.route('/gyms//', methods=['GET']) #@login_required def gym_routes_new(gym_id, routesid): + + gym = competitionsEngine.get_gym(gym_id) all_routes = competitionsEngine.get_routes_by_gym_id(gym_id) routes = all_routes.get(routesid) @@ -1398,6 +1461,8 @@ def gym_routes_new(gym_id, routesid): user_can_edit_gym = False if user is not None: user_can_edit_gym = competitionsEngine.can_edit_gym(user, gym) + #activities = skala_api.get_activities() + return render_template('gym-routes.html', gymid=gym_id, @@ -1414,7 +1479,7 @@ def gym_routes_new(gym_id, routesid): -@fsgtapp.route('/gyms//data') +@app_ui.route('/gyms//data') def gym_data(gymid): fullname = request.args.get('fullname') nick = request.args.get('nick') @@ -1429,7 +1494,7 @@ def gym_data(gymid): return json.dumps(gym) -@fsgtapp.route('/gyms//edit', methods=['GET']) +@app_ui.route('/gyms//edit', methods=['GET']) @login_required def gym_edit(gymid): gym = competitionsEngine.get_gym(gymid) @@ -1449,7 +1514,8 @@ def gym_edit(gymid): ) -@fsgtapp.route('/gyms//edit', methods=['POST']) + +@app_ui.route('/gyms//edit', methods=['POST']) @login_required def gym_save(gymid): @@ -1476,7 +1542,7 @@ def gym_save(gymid): gym = competitionsEngine.get_gym(gymid) if not competitionsEngine.can_edit_gym(user, gym): - return redirect(url_for("fsgtapp.fsgtlogin")) + return redirect(url_for("app_ui.fsgtlogin")) routes = [] for i, routeline1 in enumerate(routeline): @@ -1511,7 +1577,9 @@ def gym_save(gymid): ) -@fsgtapp.route('/gyms///edit', methods=['GET']) + +# this is the old HTML form routes editor +@app_ui.route('/gyms///edit', methods=['GET']) @login_required def gym_routes_edit(gym_id, routesid): gym = competitionsEngine.get_gym(gym_id) @@ -1531,14 +1599,15 @@ def gym_routes_edit(gym_id, routesid): -@fsgtapp.route('/gyms///edit', methods=['POST']) +#saving old type of html routes editor +@app_ui.route('/gyms///edit', methods=['POST']) @login_required def gym_routes_save(gymid, routesid): formdata = request.form.to_dict(flat=False) args1 = request.args body = request.data - bodyj = request.json + #bodyj = request.json routeid = formdata['routeid'] routeline = formdata['routeline'] @@ -1560,7 +1629,7 @@ def gym_routes_save(gymid, routesid): gym = competitionsEngine.get_gym(gymid) if not competitionsEngine.can_edit_gym(user, gym): - return redirect(url_for("fsgtapp.fsgtlogin")) + return redirect(url_for("app_ui.fsgtlogin")) routes = [] for i, routeline1 in enumerate(routeline): @@ -1588,18 +1657,11 @@ def gym_routes_save(gymid, routesid): # pickup the default routes to be rendered routes = competitionsEngine.get_routes(gym.get('routesid')) - return render_template('gym-routes.html', - gymid=gymid, - gyms=None, - gym=gym, - routes=routes, - reference_data=competitionsEngine.reference_data, - ) - + return redirect(f'/gyms/{gymid}/{routesid}') -@fsgtapp.route('/gyms///routes_csv') +@app_ui.route('/gyms///routes_csv') def downloadRoutesCsv(gym_id, routesid): gym = competitionsEngine.get_gym(gym_id) @@ -1646,7 +1708,7 @@ def downloadRoutesCsv(gym_id, routesid): -@fsgtapp.route('/gyms///download') +@app_ui.route('/gyms///download') def downloadRoutes(gym_id, routesid): gym = competitionsEngine.get_gym(gym_id) @@ -1729,7 +1791,7 @@ def flatten(x, name=''): -@fsgtapp.route('/gyms//edittest') +@app_ui.route('/gyms//edittest') def edit_test(gymid): user = competitionsEngine.get_user_by_email(session['email']) @@ -1741,7 +1803,7 @@ def user_authenticated(id, username, email, picture): competitionsEngine.user_authenticated(id, username, email, picture) -@fsgtapp.route('/gyms/add', methods=['GET']) +@app_ui.route('/gyms/add', methods=['GET']) @login_required def gyms_add_form(): user = competitionsEngine.get_user_by_email(session['email']) @@ -1760,7 +1822,7 @@ def gyms_add_form(): **session) -@fsgtapp.route('/gyms/add', methods=['POST']) +@app_ui.route('/gyms/add', methods=['POST']) @login_required def gyms_add(): user = competitionsEngine.get_user_by_email(session['email']) @@ -1768,8 +1830,8 @@ def gyms_add(): formdata = request.form.to_dict(flat=False) args1 = request.args - body = request.data - bodyj = request.json + #body = request.data + #bodyj = request.json files = request.files imgfilename = None @@ -1793,11 +1855,19 @@ def gyms_add(): routes = competitionsEngine.generate_dummy_routes(int(numberOfRoutes)) competitionsEngine.upsert_routes(routes['id'], gym_id, routes) gym = competitionsEngine.add_gym(user, gym_id, routes['id'], gymName, imgfilename, url, address, organization, []) + gym['routes'] = routes + routes_id = routes['id'] + #competitionsEngine.update_gym(gym_id, gym) + #gym2 = competitionsEngine.get_gym(gym_id) + #all_routes = competitionsEngine.get_routes_by_gym_id(gym_id) + #routes=gym2['routes'] + #gyms = competitionsEngine.get_gyms() - gyms = competitionsEngine.get_gyms() - - return render_template('gyms.html', + return render_template('gym-routes.html', competitionId=None, + gymid=gym_id, + routesid=routes_id, + gyms=None, gym=gym, routes=routes, reference_data=competitionsEngine.reference_data, @@ -1805,7 +1875,7 @@ def gyms_add(): **session) -@fsgtapp.route('/gyms//update', methods=['POST']) +@app_ui.route('/gyms//update', methods=['POST']) @login_required def gyms_update(gym_id): user = competitionsEngine.get_user_by_email(session['email']) @@ -1824,7 +1894,7 @@ def gyms_update(gym_id): **session) body = request.data - bodyj = request.json + #bodyj = request.json files = request.files delete = formdata.get('delete') save = formdata.get('save') @@ -1853,7 +1923,7 @@ def gyms_update(gym_id): competitionsEngine.delete_gym(gym_id) competitionsEngine.remove_user_permissions_to_gym(user, gym_id) os.remove(os.path.join(UPLOAD_FOLDER, gym['logo_img_id'])) - return redirect(url_for('fsgtapp.gyms')) + return redirect(url_for('app_ui.gyms')) if routesid is None or len(routesid)==0: routesid = gym['routesid'] @@ -1868,7 +1938,7 @@ def gyms_update(gym_id): gym.update((k, v) for k, v in gym_json.items() if v is not None) competitionsEngine.update_gym(gym_id, gym) - return redirect(url_for('fsgtapp.gym_by_id', gymid=gym_id)) + return redirect(url_for('app_ui.gym_by_id', gymid=gym_id)) @@ -1876,7 +1946,7 @@ def gyms_update(gym_id): -@fsgtapp.route('/competitionDashboard/loadData') +@app_ui.route('/competitionDashboard/loadData') def loadData(): competitionsEngine.init() subheader_message='data loaded' @@ -1885,10 +1955,36 @@ def loadData(): reference_data=competitionsEngine.reference_data) -@fsgtapp.route('/image/') +@app_ui.route('/image/') def image_route(img_id): #bytes_io = competitionsEngine.get_img(img_id) #return send_file(bytes_io, mimetype='image/png') #return send_file(os.path.join(UPLOAD_FOLDER, img_id)) return send_from_directory(UPLOAD_FOLDER, img_id) + + + + + +@app_ui.route('/qr', methods=['GET']) +def qr(): + try: + # Get the URL from the query string + url = request.args.get('url') + + # Create the QR code image + img = qrcode.make(url) + + buffer = io.BytesIO() + img.save(buffer) + buffer.seek(0) + + response = make_response(buffer.getvalue()) + #response.headers['Content-Disposition'] = 'attachment; filename=qr-code.png' + response.mimetype = 'image/png' + + return response + except Exception as e: + print(e) + return 'Internal Server Error', 500 \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index 32581e3..66ee08c 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -704,7 +704,7 @@ div.cs-select span { .heading-section { padding-bottom: 1em; - margin-bottom: 4em; } + margin-bottom: 2em; } .heading-section h3 { font-size: 40px; font-weight: 400; @@ -715,7 +715,8 @@ div.cs-select span { .heading-title { font-size: 40px; - margin-bottom: 1.5em; } + margin-bottom: 1.5em; +} .fh5co-property { background-repeat: no-repeat; @@ -1132,15 +1133,18 @@ div.cs-select span { .fh5co-testimonial p .text-mute { color: #ccc !important; } + + .fh5co-blog { - margin-bottom: 60px; } + margin-bottom: 60px; + background: #85b78f;} .fh5co-blog a img { width: 100%; } .fh5co-blog .blog-text { margin-bottom: 50px; positionaaa: relative; - background: #EFEFF1; + width: 100%; padding: 6px; float: right; @@ -1170,6 +1174,46 @@ div.cs-select span { +.activity-entry { + margin-bottom: 15px; + display: flex; + width: 100%; +} +.activity-entry a img { + width: 100%; +} +.activity-entry .activity-entry-text { + margin-bottom: 15px; + width: 33%; + padding: 7px; + background: #f4f9f5; + -webkit-box-shadow: 0px 10px 20px -12px rgba(0, 0, 0, 0.18); + -moz-box-shadow: 0px 10px 20px -12px rgba(0, 0, 0, 0.18); + box-shadow: 0px 10px 20px -12px rgba(0, 0, 0, 0.18); } +.activity-entry .activity-entry-text span { + display: inline-block; + margin-bottom: 20px; } +.activity-entry .activity-entry-text span.posted_by { + color: rgba(0, 0, 0, 0.5); } +.activity-entry .activity-entry-text span.comment { + float: right; } +.activity-entry .activity-entry-text span.comment a { + color: rgba(0, 0, 0, 0.5); } +.activity-entry .activity-entry-text span.comment a i { + color: #71c685; + /*padding-left: 7px; */ +} +.activity-entry-blog .activity-entry-text h3 { + font-size: 20px; + margin-bottom: 20px; + font-weight: 400; + line-height: 1.5; } +.activity-entry-blog .activity-entry-text h3 a { + color: rgba(0, 0, 0, 0.6); } + + + + #map { width: 100%; @@ -1262,7 +1306,7 @@ div.cs-select span { background: #44C662; color: #fff !important; } .btn:hover, .btn:active, .btn:focus { - background: #393e46 !important; + /*background: #393e46 !important;*/ color: #fff; outline: none !important; } .btn.btn-default:hover, .btn.btn-default:focus, .btn.btn-default:active { diff --git a/public/images/1398911_correct_mark_success_tick_valid_icon.png b/public/images/1398911_correct_mark_success_tick_valid_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f5fb23ec1bf174027ae7d006467abf005ad03b3f GIT binary patch literal 4421 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 oToKwLj##_Zk$1nV5!TJOL1`E2!NB{r; literal 0 HcmV?d00001 diff --git a/public/images/2682840_bolt_elictricity_light_lightning_storm_icon.png b/public/images/2682840_bolt_elictricity_light_lightning_storm_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..00fd969c004ae3f3ddf0c5d67d351851879b5d0c GIT binary patch literal 6526 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 z zd_iP}dolFN_3Bbbke=&~M%6jIF$@SlfWH?hv^H;hyIJ^;JRH-T+sF>Pcd1C*qp@st zJ>xMLI>3EkpL+0q=GOh}@9n0KeTZ%#G@I<4@i5c4j;&SF z)tMl7$MkBPJALym)Jgb}s>DMe6i)rFEk-TB3^>WY&tZLiDVKxc}46Q=ia(#Vc9k4!6v zqXLz8B=LWYYhdW#Nut9cl7FJdZ7YW-K)VI<3f6Kf&->y&SBC7E(!^F?NfM3PD6ikq zyfsJ^rWHUVT7k|2XcdXjhWVML-<4P#a(3sS(0j)t_6T#P<`Q>;P`rFjnn$+PfsWc2 zVqXE}&zHWG z%r;zKQVuyr9vWl%_TmPH1u)it<*BIYahrA{!xR{A8Ne1C-XnBQD$<9p!-3>nSOk4> zXY56S#V5nIJ@!UQtZ}n-@Fl8(oK_z%pHVhy`GLpE7p$3dyU*Ll4-nbz0Y+)T#2Y6UQ{gu6U^)v{Oq&j_>Z~ktO zm26bw$6dTZc==|0ztB$|oFx77EiW_n(@uFwP@Z6%hU(zGL-oqzKOIxpyAKLRjp^?& zYv5?O$(u8x9a2KOAWK!UEJOQ;Q}g!}#T&DDem98EL%OHL{-_v9?mGhw)J{^lX=7N3q4|GA$rZ@a>n=;t<5IOZu+{VD0ex6?+ zL%D_+F>U@QnrQPuECt_vffs{BYqwI=f$KaEt-6f{3Pq2+b(DF{jJ_Lqu4VV_PNSeX z#$5I*eKTZ8H7?+LZAX-Q&oq1!7X#L1_y8|y&p}MpX!%ND6PsMIKd}jRwUwaqU%c{D zb|}VdrX-^D(SW`%@41yD7nf|xdr_*V03Mna?Q9$I#yA-5T z5S@qIhDIy^;qdiYsL4U9-WV1VrXsMw(E!KdZBab$c_AK~2X-AtvoA5dc?cUglSL8@ z5c)4|TX*{V4@|UO(dU4QJ^0V-wv>UlN6S}~7xn0Xp{20MZ;e!!?>-g)I~83Cq~BUP zI;wd9spIwFz|cauWT3Sd^@7f(W<~(0+`|6XqO^fHDR_+>c_toH2H}@X9%gh^t5&{C zd?%F52cg%IU6fLuFkjv1N<%Vk_*&fE2hL(paMK2Nf&kK zmw&wGBpA?{N$&Hd7EK4_ZLVt(cc=q@CuFyr+q{8rP@!Iy)R`uq#IQKVFj%O;lf;|y zE=V1|I$dw)4jg<1XF%^V__Axe(kO_wljkq;ndR&iR9qxXiS$oDO_HZ#ST1mRtAFN? z{K(e?q1N0^^)tOz%>6tn0Tj$9{!9ZGB7=P=Kcf+eZAiWSw#flI#D>`7SwS-~gJN+4fE0lmw z)>64JhmACdJCs4Q4f4K1F*YT5j}4re;9&+g*TaxeTp1C}yvp)nbH0@qx6U!V51Gjf zn5xNz&_Q8dlSH;W46w{y1NtUih|)2J%nt)TdKRtBm^k$#H9C|)^Bz1B$^GdHENHfc xUtax(KBWa}!1EgVHLuL?f9c->s4dAHMBVI*)0&jBB~p~OwRZR|Z?EU2{{t3)bP50f literal 0 HcmV?d00001 diff --git a/public/images/4125784_confounded_confused_emoji_emotion_fail_icon.png b/public/images/4125784_confounded_confused_emoji_emotion_fail_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ccfaa34d9b34b1fcbf38b66dba3bd9b14873e3c0 GIT binary patch literal 16041 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 UH@WCkNLiero~dr#HP;9K5C7_5SO5S3 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 7dd28a5..e922919 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,100 +1,33 @@ -#appnope==0.1.3 -#argon2-cffi==21.3.0 -#argon2-cffi-bindings==21.2.0 -attrs==22.1.0 -Authlib==0.14.1 -backcall==0.2.0 -beautifulsoup4==4.11.1 -bleach==5.0.1 -certifi==2019.11.28 -#cffi==1.14.0 -#chardet==3.0.4 -#click==7.1.1 -cryptography==3.3.2 -#cycler==0.10.0 -#debugpy==1.6.3 -decorator==5.1.1 -#defusedxml==0.7.1 -entrypoints==0.4 -fastjsonschema==2.16.2 -Flask==1.1.1 -Flask-Cors==3.0.10 -Flask-Login==0.4.1 -Flask-OAuthlib==0.9.5 -gevent==21.8.0 -greenlet==1.1.3 +Authlib==1.2.1 +cachelib==0.10.2 +certifi==2023.11.17 +cffi==1.15.1 +charset-normalizer==3.3.2 +click==8.1.7 +cryptography==41.0.5 +Flask==2.2.5 +Flask-Login==0.6.3 +Flask-OAuthlib==0.9.6 +gevent==22.10.2 +greenlet==3.0.1 gunicorn==20.0.4 -#h5py==2.10.0 -idna==2.9 -importlib-metadata==5.0.0 -importlib-resources==5.9.0 -ipykernel==6.16.0 -ipython==7.34.0 -ipython-genutils==0.2.0 -ipywidgets==7.0.0 -itsdangerous==1.1.0 -jedi==0.18.1 -Jinja2==2.11.3 -joblib==0.14.1 -jsonschema==4.16.0 -#jupyter-client==7.3.5 -#jupyter-core==4.11.1 -#jupyterlab-pygments==0.2.2 -#Keras==2.3.1 -#Keras-Applications==1.0.8 -#Keras-Preprocessing==1.1.0 -#kiwisolver==1.1.0 -MarkupSafe==2.0.1 -#matplotlib==3.2.1 -#matplotlib-inline==0.1.6 -mistune==2.0.4 -nbclient==0.7.0 -#nbconvert -nbformat==5.6.1 -nest-asyncio==1.5.6 -#notebook==6.4.12 -#numpy==1.18.2 +idna==3.4 +importlib-metadata==6.7.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.3 oauthlib==2.1.0 -packaging==21.3 -pandas -pandocfilters==1.5.0 -parso==0.8.3 -pexpect==4.8.0 -pickleshare==0.7.5 -pkgutil-resolve-name==1.3.10 -#plotly==4.10.0 -prometheus-client==0.14.1 -prompt-toolkit==3.0.31 -psutil==5.9.2 -ptyprocess==0.7.0 -pycparser==2.20 -Pygments==2.13.0 -pyparsing==2.4.6 -pyrsistent==0.18.1 -python-dateutil -python-dotenv==0.12.0 -#pytz==2019.3 -#PyYAML==5.4 -pyzmq==24.0.1 -requests==2.23.0 -requests-oauthlib -retrying==1.3.3 -#scikit-learn==0.22.2.post1 -#scipy -Send2Trash==1.8.0 -six==1.14.0 -soupsieve==2.3.2.post1 -spotipy==2.10.0 -terminado==0.16.0 -tinycss2==1.1.1 -tornado==6.2 -traitlets==5.4.0 -typing-extensions==4.3.0 -urllib3 -wcwidth==0.2.5 -webencodings==0.5.1 -Werkzeug==1.0.0 -widgetsnbextension==3.0.8 -zipp==3.8.1 -zope.event==4.5.0 -zope.interface==5.1.2 +pycparser==2.21 +pydantic==2.5.3 +pydantic_core==2.14.6 +pypng==0.20220715.0 +python-dotenv==0.21.1 +qrcode==7.4.2 +requests==2.31.0 +requests-oauthlib==1.1.0 +typing_extensions==4.7.1 +urllib3==2.0.7 +Werkzeug==2.2.3 +zipp==3.15.0 +zope.event==5.0 +zope.interface==6.1 diff --git a/server.py b/server.py index 022ce8a..e2c7643 100644 --- a/server.py +++ b/server.py @@ -20,6 +20,7 @@ import urllib from functools import wraps +#import fastapi_test from dotenv import load_dotenv from flask import Flask, redirect, url_for, session, request, render_template, send_file, jsonify, Response, \ stream_with_context, copy_current_request_context @@ -37,9 +38,8 @@ import threading import random import logging -from competitionsApp import fsgtapp +from main_app_ui import app_ui, languages from skala_api import skala_api_app -from competitionsApp import languages import competitionsEngine #import locale import glob @@ -55,6 +55,8 @@ logout_user, ) +#from flask_openapi3 import OpenAPI, Info, Tag + load_dotenv() @@ -66,6 +68,9 @@ DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') +if DATA_DIRECTORY is None: + DATA_DIRECTORY = os.getcwd() + # Configuration GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID", None) GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET", None) @@ -76,8 +81,10 @@ FACEBOOK_CLIENT_ID=os.getenv("FACEBOOK_CLIENT_ID", None) FACEBOOK_CLIENT_SECRET=os.getenv("FACEBOOK_CLIENT_SECRET", None) +#info = Info(title='Skala3ma API', version='1.0.0', summary='API to interact with skala3ma', description='description of api') +#app = OpenAPI(__name__, static_folder='public', template_folder='views', info=info) app = Flask(__name__, static_folder='public', template_folder='views') -app.register_blueprint(fsgtapp) +app.register_blueprint(app_ui) app.register_blueprint(skala_api_app) app.debug = True @@ -85,6 +92,10 @@ oauth = OAuth(app) #CORS(app) +#from fastapi.middleware.wsgi import WSGIMiddleware + +#fastapi_test.fastapitest.mount("/", WSGIMiddleware(app)) + genres = {"test": "1"} authenticated = False @@ -516,7 +527,9 @@ def googleauth_reply(): # check first if auth was succesful token = oauth.google.authorize_access_token() - profile = oauth.google.parse_id_token(token) + profile1 = oauth.google.get('https://www.googleapis.com/oauth2/v1/userinfo') + profile = token.get('userinfo') + #profile = oauth.google.parse_id_token(token) print(" Google User ", profile) session['username']=profile['email'] diff --git a/skala_api.py b/skala_api.py index 2809c26..17d9920 100644 --- a/skala_api.py +++ b/skala_api.py @@ -23,6 +23,8 @@ import competitionsEngine import csv from functools import wraps +from dataclasses import dataclass + from flask import Flask, redirect, url_for, session, request, render_template, send_file, send_from_directory, jsonify, Response, \ stream_with_context, copy_current_request_context, g @@ -31,13 +33,24 @@ from dotenv import load_dotenv from flask import Blueprint -import skala_journey as journeys_engine +import activities_db as activities_db import skala_db from io import BytesIO from flask import send_file +#import Activity + +#from flask_openapi3 import APIBlueprint, OpenAPI, Tag + +#book_tag = Tag(name="book", description="Some Book") + +#comp_tag = Tag(name="competition", description=""" +# Some competition +# with multiple lines +# #header also +# """) # Third party libraries from flask import Flask, redirect, request, url_for @@ -51,6 +64,7 @@ from oauthlib.oauth2 import WebApplicationClient import requests +from pydantic import BaseModel #skala_api_app = Blueprint('skala_api_app', __name__) from authlib.integrations.flask_client import OAuth @@ -60,7 +74,14 @@ load_dotenv() +grades = ['1', '2', '3', '4a', '4b', '4c', '5a','5a+', '5b', '5c','5c+', '6a', '6a+', '6b', '6b+', '6c', '6c+', '7a', '7a+', '7b', '7b+', '7c', '7c+', '8a', '8a+', '8b', '8b+', '8c', '8c+', '9a', '9a+', '9b', '9b+', '9c'] + + DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') + +if DATA_DIRECTORY is None: + DATA_DIRECTORY = os.getcwd() + COMPETITIONS_DB = DATA_DIRECTORY + "/db/competitions.sqlite" # ID, date, name, location @@ -70,13 +91,15 @@ FSGT_APP_ID = os.getenv('FSGT_APP_ID') FSGT_APP_SECRET = os.getenv('FSGT_APP_SECRET') -DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') + GOOGLE_DISCOVERY_URL = ( "https://accounts.google.com/.well-known/openid-configuration" ) +#skala_api_app = APIBlueprint('skala_api', __name__, url_prefix='/api1', doc_ui=True, abp_tags= [book_tag, comp_tag]) skala_api_app = Blueprint('skala_api', __name__, url_prefix='/api1') + skala_api_app.debug = True skala_api_app.secret_key = 'development' oauth = OAuth(skala_api_app) @@ -109,6 +132,24 @@ # skala_api_app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +#from flask_openapi3 import Info, Tag +#from flask_openapi3 import OpenAPI + + +#info = Info(title="book API", version="1.0.0") +#book_tag = Tag(name="book", description="Some Book") + + + +class Activity1(BaseModel): + activity_name: str + gym_id: str + date: datetime + + + + + # User session management setup # https://flask-login.readthedocs.io/en/latest @@ -180,7 +221,14 @@ def decorated_function(*args, **kwargs): return decorated_function -@skala_api_app.route('/competitionRawAdmin', methods=['POST']) + +#@skala_api_app.get('/apitest', tags=[book_tag, comp_tag]) +def testapi(): + return {"code": 0, "message": "ok"} + + + +@skala_api_app.post('/competitionRawAdmin') @login_required def fsgtadmin(): edittype = request.form.get('edittype') @@ -311,46 +359,230 @@ def competition_admin_post(competition_id): id=id) -@skala_api_app.route('/journey', methods=['GET']) + +@skala_api_app.get('/activities') @login_required -def journey_list(): +def get_activities(): user = competitionsEngine.get_user_by_email(session['email']) + activitiesA = activities_db.get_activities(user.get('id')) + + activities = {} + activities['activities'] = activitiesA + + newactivities = calculate_activities_stats(activitiesA) + activities['stats'] = newactivities + return json.dumps(activities) + + +def calculate_activities_stats(activities): + # Get today's date + today = datetime.today().date() + stats = {} + # Create a dictionary with dates 30 days back from today as keys and 0 as initial values + routes_done = {(today - timedelta(days=i)).strftime('%Y-%m-%d'): 0 for i in range(30)} - journeys = journeys_engine._get_journey_sessions_by_user_id(user.get('id')) + for activity in activities: + if activity.get('date') is None: + continue + # Parse the 'date' into a date object + activity_date = activity['date'] + # If the activity date is in the routes_done dictionary, add the number of routes + if activity_date in routes_done: + routes_done[activity_date] += len(activity['routes']) + + # Convert the dictionary to a list of values + routes_done_list = list(routes_done.values()) - return json.dumps(journeys) + stats['dates'] = list(routes_done.keys()) + stats['routes_done'] = routes_done_list + return stats -@skala_api_app.route('/journey/add', methods=['POST']) + +@skala_api_app.post('/activity') @login_required def journey_add(): user = competitionsEngine.get_user_by_email(session['email']) - date = request.form.get('date') - gym_id = request.form.get('gym_id') + data = request.get_json() + #data = request.get_data() + # get the data from the body of the request + + date = data.get('date') + gym_id = data.get('gym_id') + name = data.get('activity_name') comp = {} gym = competitionsEngine.get_gym(gym_id) - journey = journeys_engine.add_journey_session(user,gym_id, gym.get('routesid'), date) + + #a = Activity1(**data) + + activity_id = activities_db.add_activity(user, gym, name, date) + activity = activities_db.get_activity(activity_id) # journey_id = user.get('journey_id') + + #journeys = activities_db.get_activities(user.get('id')) + return json.dumps(activity) + + + +@skala_api_app.delete('/activity/') +@login_required +def delete_activity(activity_id): + user = competitionsEngine.get_user_by_email(session['email']) + + activity = activities_db.get_activity(activity_id) + if (activity is None): + return {"error":"activity not found"} + #a = Activity1(**data) + + activities_db.delete_activity(activity_id) + # journey_id = user.get('journey_id') + + #journeys = activities_db.get_activities(user.get('id')) + return {} + + + + + +@skala_api_app.get('/activity/') +@login_required +def get_activity(activity_id): + user = competitionsEngine.get_user_by_email(session['email']) + + activity = activities_db.get_activity(activity_id) + if (activity is None): + return {"error":"activity not found"} + #a = Activity1(**data) + activity = activities_db.get_activity(activity_id) + #activity = calculate_activity_stats(activity) + + # journey_id = user.get('journey_id') + #journeys = activities_db.get_activities(user.get('id')) + return json.dumps(activity) + + + +@skala_api_app.post('/activity/') +@login_required +def add_activity_route(activity_id): + user = competitionsEngine.get_user_by_email(session['email']) + + data = request.get_json() + #data = request.get_data() + # get the data from the body of the request + + gym_id = data.get('gym_id') + routes_id = data.get('routes_id') + routes = competitionsEngine.get_routes(routes_id) + + route_id = data.get('route_id') + note = data.get('note') + route_finish_status = data.get('route_finish_status') + route = competitionsEngine.get_route(routes_id, route_id) + + activity = activities_db.add_activity_entry(activity_id, route, route_finish_status, note) + + # journey_id = user.get('journey_id') + #journeys = activities_db.get_activities(user.get('id')) + return json.dumps(activity) + + +@skala_api_app.get('/activity/user/') +@login_required +def get_activities_by_user(user_id): + user = competitionsEngine.get_user_by_email(session['email']) + + activity = activities_db.get_activity(activity_id) + if (activity is None): + return {"error":"activity not found"} + #a = Activity1(**data) + + activities_db.delete_activity(activity_id) + # journey_id = user.get('journey_id') + # calculate_activity_stats(activity) + #journeys = activities_db.get_activities(user.get('id')) + return {} - journeys = journeys_engine._get_journey_sessions_by_user_id(user.get('id')) - return render_template('skala-journey.html', - user=user, - journeys=journeys, - journey=journey, - reference_data=competitionsEngine.reference_data - ) @skala_api_app.route('/journey/', methods=['GET']) @login_required def journey_session(journey_id): - journey = journeys_engine.get_journey_session(journey_id) + journey = activities_db.get_journey_session(journey_id) return journey + +@skala_api_app.delete('/activity//route/') +@login_required +def delete_activity_route(activity_id, route_id): + user = competitionsEngine.get_user_by_email(session['email']) + + activity = activities_db.get_activity(activity_id) + if (activity is None): + return {"error":"activity not found"} + #a = Activity1(**data) + + activity = activities_db.delete_activity_route(activity_id, route_id) + # journey_id = user.get('journey_id') + + #journeys = activities_db.get_activities(user.get('id')) + return activity + + +def calculate_activity_stats(activity): + routes = activity.get('routes') + routes_count = len(routes) + grades_climbed = [] + for route in routes: + route['grade_index'] = grades.index(route['grade']) + #route['grade_points'] = np.exp(-((route['grade_index'] - mean) / std_dev) ** 2 / 2) + if route['status'] == 'climbed' or route['status'] == 'flashed': + grades_climbed.append(route['grade']) + + avg_grade_climbed = avg_grade(routes) + activity['stats'] = {} + activity['stats']['routes_count'] = routes_count + activity['stats']['avg_grade_climbed'] = avg_grade_climbed + + return activity + + + + +def avg_grade(routes, flash_weight=2, climb_weight=1, attempt_weight=0.1): + if not routes: + return None + + # Convert grades to indices and apply weights + weighted_indices = [] + for route in routes: + grade = route['grade'] + status = route['status'] + if status == 'flashed': + weight = flash_weight + elif status == 'climbed': + weight = climb_weight + else: # status is 'attempted' or anything else + weight = attempt_weight + weighted_indices.append(grades.index(grade) * weight) + + # Calculate average index + average_index = sum(weighted_indices) / sum(flash_weight if route['status'] == 'flashed' else climb_weight if route['status'] == 'climbed' else attempt_weight for route in routes) + + # Round to nearest integer + average_index = round(average_index) + + # Convert index back to grade + average_grade = grades[average_index] + + return average_grade + + + @skala_api_app.route('/journey//add', methods=['POST']) @login_required def journey_session_entry_add(journey_id): @@ -362,11 +594,12 @@ def journey_session_entry_add(journey_id): comp = {} - journey = journeys_engine.get_journey_session(journey_id) + journey = activities_db.get_journey_session(journey_id) - journey = journeys_engine.add_journey_session_entry(journey_id,route_id, route_finish_status, notes) + journey = activities_db.add_journey_session_entry(journey_id,route_id, route_finish_status, notes) routes = competitionsEngine.get_routes(journey.get('routes_id')) + return render_template('skala-journey-session.html', user=user, journey=journey, @@ -379,12 +612,12 @@ def journey_session_entry_add(journey_id): @login_required def journey_session_remove(journey_id, route_id): user = competitionsEngine.get_user_by_email(session['email']) - journey = journeys_engine.get_journey_session(journey_id) + journey = activities_db.get_journey_session(journey_id) testid = request.args.get('testid') if journey is None: return {} - journeys_engine.remove_journey_session(journey_id, route_id) + activities_db.remove_journey_session(journey_id, route_id) routes = competitionsEngine.get_routes(journey.get('routes_id')) return {} @@ -446,6 +679,10 @@ def competitions_by_year(year): def new_competition_post(): username = session.get('username') + routedata = request.get_json() + routedata = json.loads(routedata) + + #username = request.args.get('username') name = request.form.get('name') date = request.form.get('date') @@ -1041,8 +1278,6 @@ def get_myskala(): continue routes = routes.get('routes') all_competitions.append(competition) - - climber = competition.get('climbers').get(user['id']) competition['climber'] = climber @@ -1064,12 +1299,6 @@ def get_myskala(): competition_points_per_route = {} - - #logging.info(climbers) - - - - stats['user'] = user stats['personalstats']['competitions_count'] = len(competition_ids) @@ -1080,12 +1309,12 @@ def get_myskala(): stats['competitions'] = all_competitions + stats['thursday'] = datetime.today().weekday() == 3 + # return well formatted json + return json.dumps(stats, indent=4) - - return json.dumps(stats) - def transform_json(input_data): # Create a dictionary to hold the data for each grade diff --git a/skala_db.py b/skala_db.py index f14d268..437ae9f 100644 --- a/skala_db.py +++ b/skala_db.py @@ -23,9 +23,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import numpy as np -import pandas as pd -import numpy.random from collections import Counter import tracemalloc import sqlite3 as lite @@ -47,6 +44,10 @@ load_dotenv() DATA_DIRECTORY = os.getenv('DATA_DIRECTORY') + +if DATA_DIRECTORY is None: + DATA_DIRECTORY = os.getcwd() + #PLAYLISTS_DB = DATA_DIRECTORY + "/db/playlists.sqlite" COMPETITIONS_DB = DATA_DIRECTORY + "/db/competitions.sqlite" diff --git a/views/activities.html b/views/activities.html new file mode 100644 index 0000000..8391d8d --- /dev/null +++ b/views/activities.html @@ -0,0 +1,525 @@ + +{% extends "skala3ma-layout.html" %} + +{% block topcontent %} + +{% endblock %} + + {% block secondarycontent %} + + + + + + + + + + + + + + + + + +
    + + {% include "skala3ma-menu.html" %} + +
    + +

    {{ reference_data['current_language'].activities}} + + +

    +
    + +
    + +
    +
    + + +
    + +
    +
    + + + + +
    + + +
    + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/views/activity-detail.html b/views/activity-detail.html new file mode 100644 index 0000000..032aefd --- /dev/null +++ b/views/activity-detail.html @@ -0,0 +1,603 @@ + + + +{% extends "skala3ma-layout.html" %} + +{% block topcontent %} + +{% endblock %} + + {% block secondarycontent %} + + + + + + + + + + + + + + + + + + + + + + +
    + + {% include "skala3ma-menu.html" %} + + +
    + +

    + + + +

    +
    + + + +
    + +
    +
    + + + + + + + + + + {# here we iterate over every item in our list which we will pass from bar() #} + {% for route in activity['routes'] %} + + + + + + + + + + + + {% endfor %} + + + +
    {% if route['status'] == 'climbed' %} + climbed + {% elif route['status'] == 'attempted' %} + attempted + {% elif route['status'] == 'flashed' %} + flashed + {% endif %}{{ route['grade'] }}{{ route['name'] }}{{ route['note'] }} + + + + + + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +{% if activity is not none and activity is not undefined %} + + + {% else %} + + + No journyes yet. Add your first! + + + {% endif %} +
    + +
    + + + + + + + +{% endblock %} diff --git a/views/competitionClimber.html b/views/competitionClimber.html index 2257cec..2e0117b 100644 --- a/views/competitionClimber.html +++ b/views/competitionClimber.html @@ -1,5 +1,5 @@ - {% extends "skala3ma-layout.html" %} diff --git a/views/competitionRawAdmin.html b/views/competitionRawAdmin.html index a79624c..3f13079 100644 --- a/views/competitionRawAdmin.html +++ b/views/competitionRawAdmin.html @@ -1,5 +1,5 @@ - {% extends "skala3ma-layout.html" %} @@ -44,13 +44,15 @@

    ADMIN {{ subheader_message }}

    + +      - +

    diff --git a/views/competitionStats.html b/views/competitionStats.html index 2a9256e..1c3914f 100644 --- a/views/competitionStats.html +++ b/views/competitionStats.html @@ -176,7 +176,7 @@

    {{ reference_data['current_language'].competition_results }}
    }, }; - var chart2 = new ApexCharts(document.querySelector("#myChart2"), options2); + var chart2 = new ApexCharts(document.getElementById("myChart2"), options2); chart2.render(); @@ -554,7 +554,9 @@

    {{ reference_data['current_language'].competition_results }}
    }; - var chart3 = new ApexCharts(document.querySelector("#myChart3"), options3); + //var chart3 = new ApexCharts(document.querySelector("#myChart3"), options3); + var chart3 = new ApexCharts(document.getElementById("myChart3"), options3); + chart3.render(); diff --git a/views/gym-print.html b/views/gym-print.html index 25487b7..3582681 100644 --- a/views/gym-print.html +++ b/views/gym-print.html @@ -12,7 +12,7 @@
    - + {{ gym['name'] }}      diff --git a/views/gym-routes.html b/views/gym-routes.html index 47cd608..96ee5fc 100644 --- a/views/gym-routes.html +++ b/views/gym-routes.html @@ -123,9 +123,14 @@
       
    +
    + + +
    × + {% if (user_can_edit_gym) %}   {% endif %} @@ -406,7 +411,7 @@
    - +
    @@ -673,6 +678,47 @@

    {{ reference_data['current_language'].gyms }}

    saveData(data) } + function addActivityRoute() { + var data = getFormData(); + console.log('saving new route to activity '+mod_type) + //fetch('/api1/gym/{{gymid}}/{{routesid}}/'+mod_type, { + fetch('/api1/activity/{{gymid}}/{{routesid}}/saveone', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + .then(response => { + if (!response.ok) { + console.log('error response',response.json) + //return Promise.reject(response); + throw new Error('Network response was not ok'); + } + //return response.json(); + }) + .then(data => { + console.log('Response from saving route'); + console.log(data) + //routes=data.routes + //renderTabulatorTable(routes); + //table.replaceData(routes); + hideModal() + + }) + .catch(error => { + console.error('catch error:', error); + document.getElementById('hidden_message').textContent="{{ reference_data['current_language']['error5315'] }}"; + + + + }); + } + + + + + function saveData(data){ console.log('saving data '+mod_type) //fetch('/api1/gym/{{gymid}}/{{routesid}}/'+mod_type, { @@ -742,7 +788,7 @@

    {{ reference_data['current_language'].gyms }}

    aaaa - {{ gyms[gymid]['date'] }} diff --git a/views/gymedit.html b/views/gymedit.html index a0debfa..5e8447b 100644 --- a/views/gymedit.html +++ b/views/gymedit.html @@ -113,7 +113,7 @@

    {{ gym['name'] }}


    - +

    diff --git a/views/gyms.html b/views/gyms.html index 9a3f33c..144e633 100644 --- a/views/gyms.html +++ b/views/gyms.html @@ -79,7 +79,7 @@

    {% if gym is not none and gym is not undefined %} - + {{ gym['name'] }} @@ -170,7 +170,7 @@

    {{ reference_data['current_language'].gyms }}

    - {{ gyms[gymid]['date'] }} diff --git a/views/myskala.html b/views/myskala.html index 603103e..5aa99f5 100644 --- a/views/myskala.html +++ b/views/myskala.html @@ -202,14 +202,14 @@

    {{ reference_data['current_language'].my_resultats}}

    options.series[0].data[0].y=fulldata['personalstats']['total_competitions_count'] options.series[0].data[0].goals[0].value=fulldata['personalstats']['competitions_count'] - var chart = new ApexCharts(document.querySelector("#chartbox"), options); + var chart = new ApexCharts(document.getElementById("chartbox"), options); chart.render(); options.xaxis.title.text='{{ reference_data["current_language"].stat_title_competition_routes_climbed}}'; options.series[0].data[0].y=fulldata['personalstats']['competition_routes_total'] options.series[0].data[0].goals[0].value=fulldata['personalstats']['routes_climbed_count'] - var chart2 = new ApexCharts(document.querySelector("#chartbox2"), options); + var chart2 = new ApexCharts(document.getElementById("chartbox2"), options); chart2.render(); diff --git a/views/skala-journey.html b/views/skala-journey.html index eb7722f..f280cda 100644 --- a/views/skala-journey.html +++ b/views/skala-journey.html @@ -9,12 +9,93 @@ {% block secondarycontent %} + + + +
    {% include "skala3ma-menu.html" %} - -
    + +
    +
    + + +
    + +
    +
    adsfasdf
    alfasdf
    +
    +
    +
    adsfasdf
    alfasdf
    +
    +
    +
    adsfasdf
    alfasdf
    +
    + +
    + + +

    Your climbing journey

    {{ reference_data['current_language'].start }} diff --git a/views/skala3ma-layout.html b/views/skala3ma-layout.html index c290564..3d552ce 100644 --- a/views/skala3ma-layout.html +++ b/views/skala3ma-layout.html @@ -3,9 +3,9 @@ - + content="default-src 'self'; img-src https://*; child-src 'none';" /--> skala3ma @@ -48,6 +48,9 @@ rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" /> + + + +
  • + {{ reference_data['current_language'].activities }} +
  • @@ -265,7 +268,7 @@

    {{ session['name'] }} +
  • @@ -371,8 +374,8 @@

    Links

    Languages

    -     -     + +