Compare commits
6 Commits
6a52b0cba5
...
c9d4c335ab
Author | SHA1 | Date |
---|---|---|
Samuel Ortion | c9d4c335ab | |
Samuel Ortion | 3e80e68da7 | |
Samuel Ortion | a760e2fabc | |
Samuel Ortion | 899487245d | |
Samuel Ortion | 3a8faf6d5f | |
Samuel Ortion | 13e7c35a1a |
|
@ -1,3 +1,5 @@
|
|||
venv
|
||||
config.py
|
||||
__pycache__
|
||||
__pycache__
|
||||
src_audio/
|
||||
node_modules
|
|
@ -33,11 +33,6 @@ cp config.py.example config.py
|
|||
## Database migration
|
||||
|
||||
```bash
|
||||
python3
|
||||
```
|
||||
|
||||
```python3
|
||||
from app import *
|
||||
app.app_context().push()
|
||||
db.create_all()
|
||||
flask db migrate -m "Migration message."
|
||||
flask db upgrade # Perform migration (after script verification in ./migrations/versions/)
|
||||
```
|
95
app.py
95
app.py
|
@ -4,11 +4,16 @@ from flask import session
|
|||
from flask import request
|
||||
from flask import redirect
|
||||
from flask import url_for
|
||||
from flask import g
|
||||
from flask_migrate import Migrate
|
||||
|
||||
from flask_babel import Babel, gettext
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from config import secret_key, database_uri
|
||||
from model import db, User
|
||||
import game as Game
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = secret_key
|
||||
|
@ -16,8 +21,38 @@ app.secret_key = secret_key
|
|||
app.config['SQLALCHEMY_DATABASE_URI'] = database_uri
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
|
||||
app.config['BABEL_TRANSLATION_DIRECTORIES'] = "./language/translations"
|
||||
babel = Babel(app)
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
if not session.get('lang') is None:
|
||||
return session['lang']
|
||||
# otherwise try to guess the language from the user accept
|
||||
# header the browser transmits. We support fr/en in this
|
||||
# example. The best match wins.
|
||||
return request.accept_languages.best_match(['fr', 'en'])
|
||||
|
||||
@babel.timezoneselector
|
||||
def get_timezone():
|
||||
user = getattr(g, 'user', None)
|
||||
if user is not None:
|
||||
return user.timezone
|
||||
|
||||
app.jinja_env.globals['get_locale'] = get_locale
|
||||
|
||||
@app.route('/lang')
|
||||
def get_lang():
|
||||
return render_template('lang.html')
|
||||
|
||||
@app.route('/lang/<locale>')
|
||||
def set_lang(locale='en'):
|
||||
session['lang'] = locale
|
||||
return redirect('/')
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
if 'username' in session:
|
||||
|
@ -28,17 +63,29 @@ def home():
|
|||
@app.route("/signup", methods=["GET", "POST"])
|
||||
def signup():
|
||||
if request.method == "POST":
|
||||
message = ""
|
||||
username = request.form['username']
|
||||
if username == "":
|
||||
message += gettext("Username empty. Try to find one.")
|
||||
email = request.form['email']
|
||||
if email == "":
|
||||
message += gettext("Email empty. Please give me one.")
|
||||
password = request.form['password']
|
||||
registered_user = User.query.filter_by(username=username).first()
|
||||
if registered_user is None:
|
||||
if password == "":
|
||||
message += gettext("You should not use an empty password")
|
||||
registered_user_by_username = User.query.filter_by(user_name=username).first()
|
||||
registered_user_by_email = User.query.filter_by(user_email=email).first()
|
||||
if registered_user_by_username is None and registered_user_by_email is None:
|
||||
password_hash = generate_password_hash(password)
|
||||
registered_user = User(username=username, email=email, password=password_hash)
|
||||
registered_user = User(user_name=username, user_email=email, user_password=password_hash)
|
||||
db.session.add(registered_user)
|
||||
db.session.commit()
|
||||
else:
|
||||
return render_template("auth/signup.html", message="Username already used. Try with an other.")
|
||||
if not registered_user_by_email is None:
|
||||
message += gettext("Email already used by a user.")
|
||||
else:
|
||||
message += gettext("Username already used by a user.")
|
||||
return render_template("auth/signup.html", message=message)
|
||||
return redirect(url_for("login"))
|
||||
elif request.method == "GET":
|
||||
return render_template("auth/signup.html")
|
||||
|
@ -48,11 +95,11 @@ def login():
|
|||
if request.method == "POST":
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
user = User.query.filter_by(username=username).first()
|
||||
user = User.query.filter_by(user_name=username).first()
|
||||
if user is None:
|
||||
return render_template("auth/login.html", message="No user with this username already registered")
|
||||
else:
|
||||
password_hash = user.password
|
||||
password_hash = user.user_password
|
||||
if check_password_hash(password_hash, password):
|
||||
session["username"] = username
|
||||
return redirect(url_for("home"))
|
||||
|
@ -65,4 +112,38 @@ def login():
|
|||
def logout():
|
||||
# Remove username from the session if it's there
|
||||
session.pop("username", None)
|
||||
return redirect(url_for("home"))
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# Game routes
|
||||
|
||||
@app.route("/game")
|
||||
def game():
|
||||
return redirect(url_for('new_game'))
|
||||
|
||||
@app.route("/game/new")
|
||||
def new_game():
|
||||
if not "username" in session:
|
||||
return redirect(url_for('login'))
|
||||
user = User.query.filter_by(user_name = session['username']).first()
|
||||
if user is None:
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
level = str(user.user_level)
|
||||
question = Game.new_question(level)
|
||||
session["question"] = question
|
||||
return render_template("game/question.html", question=question)
|
||||
|
||||
@app.route("/game/answer", methods=["POST", "GET"])
|
||||
def game_answer():
|
||||
if request.method == "POST":
|
||||
answer = request.form["answer"]
|
||||
if answer == session["question"]["species"]:
|
||||
message = "You are correct !"
|
||||
else:
|
||||
message = "You are not correct !"
|
||||
return render_template("game/answer.html", message=message, question=session["question"])
|
||||
elif request.method == "GET":
|
||||
return render_template("game/new.html")
|
||||
|
||||
|
||||
migrate = Migrate(app, db)
|
|
@ -0,0 +1,3 @@
|
|||
[python: **.py]
|
||||
[jinja2: templates/**.html]
|
||||
encoding = utf-8
|
|
@ -0,0 +1,120 @@
|
|||
{
|
||||
"1": [
|
||||
"Buse variable",
|
||||
"Merle noir",
|
||||
"Pic vert",
|
||||
"Rougegorge familier",
|
||||
"Mouette rieuse",
|
||||
"Canard colvert",
|
||||
"Pinson des arbres",
|
||||
"Moineau domestique",
|
||||
"Pie bavarde",
|
||||
"Corneille noire",
|
||||
"Faisan de Colchide"
|
||||
],
|
||||
"2": [
|
||||
"Grive draine",
|
||||
"Grande Aigrette",
|
||||
"Grand Cormoran",
|
||||
"Foulque macroule",
|
||||
"Hirondelle rustique",
|
||||
"Fauvette grisette",
|
||||
"Troglodyte mignon",
|
||||
"Bergeronnette grise",
|
||||
"Choucas des tours",
|
||||
"Pigeon ramier",
|
||||
"Rougequeue noir",
|
||||
"Tourterelle turque"
|
||||
],
|
||||
"3": [
|
||||
"Aigrette garzette",
|
||||
"Grive litorne",
|
||||
"Accenteur mouchet",
|
||||
"Bernache du Canada",
|
||||
"Pipit farlouse",
|
||||
"Tourterelle des bois",
|
||||
"Sittelle torchepot",
|
||||
"Alouette des champs",
|
||||
"Coucou gris",
|
||||
"Corbeau freux",
|
||||
"Martinet noir",
|
||||
"Bruant zizi"
|
||||
],
|
||||
"4": [
|
||||
"Grimpereau des jardins",
|
||||
"Grive musicienne",
|
||||
"Bruant jaune",
|
||||
"Bergeronnette des ruisseaux",
|
||||
"Sterne pierregarin",
|
||||
"Bruant des roseaux",
|
||||
"Milan noir",
|
||||
"Grand Corbeau",
|
||||
"Fuligule morillon"
|
||||
],
|
||||
"5": [
|
||||
"Gobemouche gris",
|
||||
"Cigogne blanche",
|
||||
"Alouette lulu",
|
||||
"Grive mauvis",
|
||||
"Tarin des aulnes",
|
||||
"Chouette hulotte",
|
||||
"Traquet motteux",
|
||||
"Milan royal",
|
||||
"Monticole bleu",
|
||||
"Canard chipeau"
|
||||
],
|
||||
"6": [
|
||||
"Fuligule milouin",
|
||||
"Chevalier guignette",
|
||||
"Bouscarle de Cetti",
|
||||
"Tadorne de Belon",
|
||||
"Cisticole des joncs",
|
||||
"Pic noir",
|
||||
"Nette rousse",
|
||||
"Petit Gravelot",
|
||||
"Fauvette des jardins",
|
||||
"Serin cini"
|
||||
],
|
||||
"7": [
|
||||
"Pic mar",
|
||||
"Perdrix grise",
|
||||
"Pinson du Nord",
|
||||
"Bruant proyer",
|
||||
"Phragmite des joncs",
|
||||
"Rousserolle effarvatte",
|
||||
"Pouillot fitis",
|
||||
"Faucon hobereau"
|
||||
],
|
||||
"8": [
|
||||
"Fauvette pitchou",
|
||||
"Busard des roseaux",
|
||||
"Canard mandarin",
|
||||
"Spatule blanche",
|
||||
"Bernache nonnette",
|
||||
"Torcol fourmilier",
|
||||
"Effraie des clochers",
|
||||
"Sittelle corse",
|
||||
"Perdrix rouge",
|
||||
"Hirondelle de rochers",
|
||||
"Hirondelle de rivage"
|
||||
],
|
||||
"9": [
|
||||
"Moineau friquet",
|
||||
"Bernache cravant",
|
||||
"Plongeon imbrin",
|
||||
"Fuligule nyroca",
|
||||
"Pigeon colombin",
|
||||
"Grimpereau des bois"
|
||||
],
|
||||
"10": [
|
||||
"Petit-duc scops",
|
||||
"Chevalier sylvain",
|
||||
"Chevalier gambette",
|
||||
"Pipit spioncelle",
|
||||
"Canard souchet",
|
||||
"Accenteur alpin",
|
||||
"Sterne naine",
|
||||
"Chevalier aboyeur",
|
||||
"Gobemouche noir"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import random
|
||||
import os
|
||||
from glob import glob
|
||||
import json
|
||||
|
||||
with open("./data/level_species_cleaned.json", "r") as f:
|
||||
LEVEL_SPECIES_LIST = json.load(f)
|
||||
|
||||
format_name = lambda folder_name : folder_name.replace('_', ' ')
|
||||
to_folder_name = lambda species_name : species_name.replace(' ', '_').replace('\'', '').lower()
|
||||
|
||||
def get_proposals(question_species_name, available_species, n):
|
||||
proposals = [question_species_name]
|
||||
for i in range(n):
|
||||
proposition = random.choice(available_species)
|
||||
while proposition == question_species_name:
|
||||
proposition = random.choice(available_species)
|
||||
proposals.append(proposition)
|
||||
random.shuffle(proposals)
|
||||
return proposals
|
||||
|
||||
def new_question(level):
|
||||
available_species = LEVEL_SPECIES_LIST[level]
|
||||
question_species_name = random.choice(available_species)
|
||||
question_species_folder = to_folder_name(question_species_name)
|
||||
question = {}
|
||||
audio_paths = list(map(os.path.basename, glob(f"static/data/src_audio/{question_species_folder}/*.mp3")))
|
||||
audio_path = random.choice(audio_paths)
|
||||
question["species"] = question_species_name
|
||||
question["species_folder"] = question_species_folder
|
||||
question["audio_path"] = audio_path
|
||||
question["proposals"] = get_proposals(question_species_name, available_species, 5)
|
||||
return question
|
|
@ -0,0 +1,51 @@
|
|||
# Translations template for PROJECT.
|
||||
# Copyright (C) 2022 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2022-05-27 10:03+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.10.1\n"
|
||||
|
||||
#: templates/base.html:17
|
||||
msgid "Welcome"
|
||||
msgstr ""
|
||||
|
||||
#: templates/index.html:3
|
||||
msgid "Welcome to BirdQuizz !"
|
||||
msgstr ""
|
||||
|
||||
#: templates/menu.html:7
|
||||
msgid "Home"
|
||||
msgstr ""
|
||||
|
||||
#: templates/menu.html:8
|
||||
msgid "Game"
|
||||
msgstr ""
|
||||
|
||||
#: templates/menu.html:9
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: templates/menu.html:11
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: templates/menu.html:13
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: templates/menu.html:15
|
||||
msgid "Identify bird song"
|
||||
msgstr ""
|
||||
|
Binary file not shown.
|
@ -0,0 +1,79 @@
|
|||
# French translations for PROJECT.
|
||||
# Copyright (C) 2022 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2022-05-27 10:03+0200\n"
|
||||
"PO-Revision-Date: 2022-05-26 13:10+0200\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: fr\n"
|
||||
"Language-Team: fr <LL@li.org>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.10.1\n"
|
||||
|
||||
#: templates/base.html:17
|
||||
msgid "Welcome"
|
||||
msgstr "Bienvenue"
|
||||
|
||||
#: templates/index.html:3
|
||||
msgid "Welcome to BirdQuizz !"
|
||||
msgstr "Bienvenue au BirdQuizz !"
|
||||
|
||||
#: templates/menu.html:7
|
||||
msgid "Home"
|
||||
msgstr "Accueil"
|
||||
|
||||
#: templates/menu.html:8
|
||||
msgid "Game"
|
||||
msgstr "Jeu"
|
||||
|
||||
#: templates/menu.html:9
|
||||
msgid "About"
|
||||
msgstr "À propos"
|
||||
|
||||
#: templates/menu.html:11
|
||||
msgid "Logout"
|
||||
msgstr "Déconnexion"
|
||||
|
||||
#: templates/menu.html:13
|
||||
msgid "Login"
|
||||
msgstr "Connexion"
|
||||
|
||||
#: templates/menu.html:15
|
||||
msgid "Identify bird song"
|
||||
msgstr "Indentifie les son d'oiseaux"
|
||||
|
||||
#~ msgid "Welcome"
|
||||
#~ msgstr "Bienvenue"
|
||||
|
||||
#~ msgid "Welcome to BirdQuizz !"
|
||||
#~ msgstr "Bienvenue au BirdQuizz !"
|
||||
|
||||
#~ msgid "Home"
|
||||
#~ msgstr "Accueil"
|
||||
|
||||
#~ msgid "Game"
|
||||
#~ msgstr "Jeu"
|
||||
|
||||
#~ msgid "Username empty. Try to find one."
|
||||
#~ msgstr "Nom d'utilisateur vide. Essayez d'en trouver un."
|
||||
|
||||
#~ msgid "Email empty. Please give me one."
|
||||
#~ msgstr "Email non fourmi. Merci de m'en envoyez un."
|
||||
|
||||
#~ msgid "You should not use an empty password"
|
||||
#~ msgstr "Vous ne devriez pas utiliser un mot de passe vide."
|
||||
|
||||
#~ msgid "Email already used by a user."
|
||||
#~ msgstr "Cet email a déjà été utilisé par quelqu'un."
|
||||
|
||||
#~ msgid "Username already used by a user."
|
||||
#~ msgstr "Ce nom d'utilisateur a déjà été pris."
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
flask db migrate -m "Initial migration."
|
||||
flask db upgrade
|
|
@ -0,0 +1 @@
|
|||
Single-database configuration for Flask.
|
|
@ -0,0 +1,50 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
|
@ -0,0 +1,91 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.get_engine().url).replace(
|
||||
'%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = current_app.extensions['migrate'].db.get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
11
model.py
11
model.py
|
@ -1,11 +1,14 @@
|
|||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), unique=True)
|
||||
email = db.Column(db.String(64), unique=True, index=True)
|
||||
password = db.Column(db.String(128))
|
||||
user_id = db.Column(db.Integer, primary_key=True)
|
||||
user_name = db.Column(db.String(64), unique=True)
|
||||
user_email = db.Column(db.String(64), unique=True, index=True)
|
||||
user_password = db.Column(db.String(128))
|
||||
user_level = db.Column(db.Integer, default=1)
|
||||
user_score = db.Column(db.Integer, default=0)
|
|
@ -1,12 +1,18 @@
|
|||
alembic==1.7.7
|
||||
Babel==2.10.1
|
||||
click==8.1.3
|
||||
Flask==2.1.2
|
||||
Flask-Babel==2.0.0
|
||||
Flask-Migrate==3.1.0
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
greenlet==1.1.2
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
Mako==1.2.0
|
||||
MarkupSafe==2.1.1
|
||||
mysql-connector-python==8.0.29
|
||||
protobuf==3.20.1
|
||||
PyMySQL==1.0.2
|
||||
pytz==2022.1
|
||||
SQLAlchemy==1.4.36
|
||||
Werkzeug==2.1.2
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
main {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 1em;
|
||||
}
|
|
@ -9,11 +9,13 @@
|
|||
{% endif %}
|
||||
|
||||
<form action="/login" method="POST">
|
||||
<label for="username">Username</label>
|
||||
<label for="username">{{ _('Username') }}</label>
|
||||
<input type="text" name="username" id="username">
|
||||
<label for="password">Password</label>
|
||||
<label for="password">{{ _('Password') }}</label>
|
||||
<input type="password" name="password" id="password">
|
||||
<input type="submit" value="Login">
|
||||
<input type="submit" value="{{ _('Login') }}">
|
||||
</form>
|
||||
|
||||
<a href="/signup">{{ _('Sign up') }}</a>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
{% endif %}
|
||||
|
||||
<form action="/signup" method="POST">
|
||||
<label for="username">Username</label>
|
||||
<label for="username">{{ _('Username') }}</label>
|
||||
<input type="text" name="username" id="username">
|
||||
<label for="email">Email</label>
|
||||
<label for="email">{{ _('Email') }}</label>
|
||||
<input type="email" name="email" id="email">
|
||||
<label for="password">Password</label>
|
||||
<label for="password">{{ _('Password') }}</label>
|
||||
<input type="password" name="password" id="password">
|
||||
<input type="submit" value="Sign up">
|
||||
<input type="submit" value="{{ _('Sign up') }}">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,9 +9,10 @@
|
|||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{% include 'menu.html' %}
|
||||
<h1>BirdQuizz</h1>
|
||||
{% if username is defined %}
|
||||
<p>Welcome <span class="username">{{ username }}</span></p>
|
||||
<p>{{ _('Welcome') }} <span class="username">{{ username }}</span></p>
|
||||
{% endif %}
|
||||
</header>
|
||||
<main>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<p>{{ message }}</p>
|
||||
<p>
|
||||
<a href="/game/new">{{ _('New Question') }}</a>
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
<a href="/game/new" class="button">
|
||||
{{ _('New Game') }}
|
||||
</a>
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<audio src="/static/data/src_audio/{{ question['species_folder'] }}/{{ question['audio_path'] }}" controls autoplay></audio>
|
||||
<form action="/game/answer" method="POST">
|
||||
<select name="answer" id="answer">
|
||||
{% for item in question['proposals'] %}
|
||||
<option value="{{ item }}">{{ item }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="submit" value="{{ _('Send Answer') }}">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -1,5 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
Welcome to BirdQuizz !
|
||||
{{ _('Welcome to BirdQuizz !') }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>{{ _('Select your language !') }}</h2>
|
||||
<form action="/lang" method="POST">
|
||||
<select name="lang" id="lang">
|
||||
<option value="fr">Français</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
<input type="submit" value="{{ _('Translate') }}">
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
|||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">{{ _('Home') }}</a></li>
|
||||
<li><a href="/game">{{ _('Game') }}</a></li>
|
||||
{% if username is defined %}
|
||||
<li><a href="/logout">{{ _('Logout') }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="/login">{{ _('Login') }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Update levels species folowing the folder names"""
|
||||
|
||||
import json
|
||||
import glob
|
||||
import os
|
||||
|
||||
SPECIES_FOLDER_NAMES = list(map(os.path.basename, glob.glob("./static/data/src_audio/*")))
|
||||
|
||||
def filter(species, condition):
|
||||
keeped = []
|
||||
for name in species:
|
||||
if condition(name):
|
||||
keeped.append(name)
|
||||
return keeped
|
||||
|
||||
def only_matched_folder(species_name):
|
||||
folder_like_name = species_name.replace(' ', '_').replace('\'', '')
|
||||
folder_like_name = folder_like_name.lower()
|
||||
return folder_like_name in SPECIES_FOLDER_NAMES
|
||||
|
||||
def main():
|
||||
with open("./data/level_species.json", "r") as f:
|
||||
data = json.load(f)
|
||||
for level in data:
|
||||
species_list = data[level]
|
||||
species_list = filter(species_list, only_matched_folder)
|
||||
data[level] = species_list
|
||||
with open("./data/level_species_cleaned.json", "w") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate species list for each levels"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import json
|
||||
import math
|
||||
|
||||
LEVELS = 10
|
||||
|
||||
def load_species_sightings(file):
|
||||
species_sightings = {}
|
||||
with open(file, "r") as f:
|
||||
data = json.load(f)
|
||||
sightings = data['data']['sightings']
|
||||
counter = 0
|
||||
for sighting in sightings:
|
||||
species = sighting["species"]["name"]
|
||||
if species in species_sightings:
|
||||
species_sightings[species] += 1
|
||||
else:
|
||||
species_sightings[species] = 1
|
||||
counter += 1
|
||||
for species in species_sightings:
|
||||
species_sightings[species] /= counter * 0.01
|
||||
return species_sightings
|
||||
|
||||
def split_species_list(species_sightings):
|
||||
level_lists = { level: [] for level in range(1, LEVELS + 1)}
|
||||
species_sorted = sorted(species_sightings, key = lambda species: -species_sightings[species])
|
||||
species_number = len(species_sorted)
|
||||
species_per_level = species_number // LEVELS
|
||||
species_splitted = [species_sorted[i:i+species_per_level] for i in range(0, species_number, species_per_level)]
|
||||
for level in range(1, LEVELS + 1):
|
||||
level_lists[level] = species_splitted[level - 1]
|
||||
return level_lists
|
||||
|
||||
def main():
|
||||
file = "./data/export_26052022_150619.json"
|
||||
species_frequency = load_species_sightings(file)
|
||||
level_species = split_species_list(species_frequency)
|
||||
with open("./data/level_species.json", "w") as f:
|
||||
json.dump(level_species, f, ensure_ascii=False, indent=4)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
flask db migrate -m "Initial migration."
|
||||
flask db upgrade
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
pybabel extract -F babel.cfg -o ./language/message.pot ./templates/**
|
||||
pybabel update -i message.pot -d translations
|
||||
pybabel compile -d translations
|
Loading…
Reference in New Issue