initial commit

This commit is contained in:
Florian du Garage Num 2023-04-13 15:34:46 +02:00
commit 07ccbd3db1
51 changed files with 2669 additions and 0 deletions

1
.flaskenv Normal file
View File

@ -0,0 +1 @@
FLASK_APP=project.app:create_app

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
actual.env
.env
.venv
venv
data.db
*__pycache__*

20
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"args": [
"run",
"--debug",
],
"jinja": true,
"justMyCode": true
}
]
}

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: gunicorn project.app:app

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# Flask Secure application
This is a template for a Flask application, using Flask-Security-Too and SQLAlchemy.
## Usage
1. Clone the project
2. Rename prod.env or dev.env to .env
3. In .env, modify what you need to
4. Create and activate python virtual env
5. Install requirements with `pip install -r requirements.txt`
6. Now you can run your app with `flask run`

26
model.env Normal file
View File

@ -0,0 +1,26 @@
AIRTABLE_API_KEY="keyXXXXXXXXXXX"
AIRTABLE_BASE_ID="appXXXXXXXXXXX"
AIRTABLE_TABLE_NAME="my_table_name"
APP_SETTINGS="project.config.DevelopmentConfig"
DATABASE_URL="sqlite:///data.db"
DEV_SECRET_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
DEV_SECURITY_PASSWORD_SALT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
DOKKU_APP_RESTORE="1"
DOKKU_APP_TYPE="herokuish"
DOKKU_PROXY_PORT="80"
DOKKU_PROXY_PORT_MAP="http:80:5000 https:443:5000"
DOKKU_PROXY_SSL_PORT="443"
FLASK_APP="project.app"
GIT_REV="a7ab18abec8faf898ade0d4c4afbd15c16306287"
GOOGLE_OAUTH_CLIENT_ID="xxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
GOOGLE_OAUTH_CLIENT_SECRET="GOCSPX-XXXXXX-XXXXXXXXXXXXXXXXX"
MAIL_PASSWORD="xxxxxxxxxxxxxxxxxx"
MAIL_PORT="587"
MAIL_SERVER="mail.xx.xx"
MAIL_USERNAME="mail@mail.xx"
MAIL_USE_TLS="True"
OAUTHLIB_INSECURE_TRANSPORT="true"
PRODUCTION_SECRET_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx"
PRODUCTION_SECURITY_PASSWORD_SALT="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx"
SQLALCHEMY_ECHO="True"
ALLOWED_USERS=["user@test.xx, "admin@test.xx"]

55
project/admin.py Normal file
View File

@ -0,0 +1,55 @@
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_security import Security, current_user
from wtforms.fields import PasswordField
from flask import current_app
from database import db_session
from models import User, Role
#<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.css" integrity="sha512-SI0aF82pT58nyOjCNfyeE2Y5/KHId8cLIX/1VYzdjTRs0HPNswsJR+aLQYSWpb88GDJieAgR4g1XWZvUROQv1A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
bulma = ["https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.css"]
current_app.config['FLASK_ADMIN_EXTRA_CSS'] = bulma
admin = Admin(current_app, name='Admin', template_mode='bootstrap4')
class UserModelView(ModelView):
column_list = ['email','role']
create_modal = False
can_view_details = True
extra_css = ["https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.css"]
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for('security.login', next=request.url))
def scaffold_form(self):
form_class = super(UserModelView, self).scaffold_form()
form_class.password2 = PasswordField('New Password')
return form_class
def on_model_change(self, form, model, is_created):
if len(model.password2):
model.password = utils.encrypt_password(model.password2)
def get_save_return_url(self, model, is_created):
return self.get_url('.details_view', id=model.id)
# define a context processor for merging flask-admin's template context into the
# flask-security views.
security = current_app.security
@security.context_processor
def security_context_processor():
def get_url(admin, **kwargs):
return url_for(admin, **kwargs)
return dict(
admin_base_template=admin.base_template,
admin_view=admin.index_view,
h=admin_helpers,
get_url=url_for
)
admin.add_view(UserModelView(User, db_session))

224
project/airtableget.py Normal file
View File

@ -0,0 +1,224 @@
from flask import Blueprint, render_template, request, redirect, url_for, current_app
from flask_security import current_user, auth_required
from werkzeug.utils import secure_filename
from airtable import Airtable
from dotenv import load_dotenv
import requests
import os
from base64 import b64encode
import json
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(os.path.dirname(basedir), '.env'))
api_key = os.environ.get("AIRTABLE_API_KEY" or '')
base_id = os.environ.get("AIRTABLE_BASE_ID" or '')
airtable_bp = Blueprint("airtable", __name__, template_folder="templates/airtable")
table_call = {
'participants': "Participants",
'profs': "Professeurs",
'formations': "Formations",
'parcours': "Spécialités",
'sessions': "Sessions",
'modules': "Matières",
'ateliers': "Séances",
'groupes': "Groupes",
'log_ateliers': "Suivi_séances",
'skills': "Compétences",
'villes': "Villes",
'log_modules': "Suivi_matières",
'log_skills': "Suivi_compétences",
'log_participants': "Bulletins"
}
def fetch_api(table):
data = Airtable(base_id, table_call[table], api_key)
return data
def upload_files(files):
urls = []
for filename, storage in files:
filename = secure_filename(filename)
filepath = os.path.join(current_app.static_folder, filename)
storage.save(filepath)
file_url = request.url_root.rstrip('/') + url_for('static', filename=filename)
attachment = {'url': file_url, 'filename': filename}
urls.append(attachment)
print("urls uploaded: " + str(urls))
return urls
def update_airtable_attachment_field(request, record, field_name, airtable_column_name=None):
airtable_column_name = airtable_column_name or field_name
uploaded_files = list(request.files.items()) or []
# set of files sent via input fields
set_input = set(file_id[len(field_name) + 1:] for file_id, file_storage in uploaded_files if file_id.startswith(field_name))
if record.get('fields').get(airtable_column_name):
# set of airtable attachments ids
set_airtable = set(file['id'] for file in record['fields'][airtable_column_name])
else:
set_airtable = set()
print("set_airtable: " + str(set_airtable))
print("set_input: " + str(set_input))
# Éléments présents dans set_airtable mais pas dans set_input
to_remove = list(set_airtable - set_input)
print("Éléments dans airtable mais pas dans le formulaire:", to_remove)
# Éléments présents dans set_input mais pas dans set_airtable
to_add = list(set_input - set_airtable)
print("Éléments dans le formulaire mais pas dans airtable:", to_add, "(len: ", len(to_add), " )")
base_data = [ attachment for attachment in record['fields'].get(airtable_column_name, [])]
print("base_data: " + str(base_data))
data = [ attachment for attachment in record['fields'].get(airtable_column_name, []) if attachment['id'] not in to_remove]
print(" ")
print("======================================")
print("base_data after filter: " + str(base_data))
if len(to_add) > 0:
files = [file for file in uploaded_files if file[0][len(field_name) + 1:] in to_add]
new_attachments = upload_files(files)
data.extend(new_attachments)
print("======================================")
print("data + " + airtable_column_name + " : " + str(data))
return data
@airtable_bp.route('/dashboard', methods=['GET'])
@auth_required()
def airtable_dashboard():
return render_template('dashboard.html')
@airtable_bp.route('/participants', methods=['GET'])
@auth_required()
def display_participants():
participants = fetch_api('participants')
print("Participants: " + str(participants))
records = []
for participant in participants.get_all():
records.append(participant)
return render_template('participants/participants_list.html', records=records)
@airtable_bp.route("/participants/<participant_number>", methods=['GET'])
@auth_required()
def display_participant_form(participant_number):
participants = fetch_api('participants')
sessions = fetch_api('sessions')
participant = participants.search('Number', participant_number)[0]
return render_template('participants/participants_detail.html', record=participant, sessions=sessions)
@airtable_bp.route('/participants/<participant_number>/edit', methods=['GET', 'POST'])
@auth_required()
def edit_participant(participant_number):
participants = fetch_api('participants')
record = participants.search('Number', participant_number)[0]
record_id = record['id']
#print("record: " + str(record_id) + " - record_CNI: " + str(record['fields']['CNI']))
if request.method == 'POST':
print("======================================")
print("request.form elements: ")
for field, value in request.form.items():
print(f'{field}: {value}')
print("======================================")
print("request files: ")
for field, value in request.files.items():
print(f'{field}: {value}')
print("======================================")
data = {}
if request.form['first_name'] != record['fields']['Prénom']:
data['Prénom'] = request.form['first_name']
if request.form['family_name'] != record['fields']['Nom']:
data['Nom de famille'] = request.form['family_name']
data['CNI'] = update_airtable_attachment_field(request, record, "CNI")
data["Attestation Pôle Emploi"] = update_airtable_attachment_field(request, record, "Attestation", "Attestation Pôle Emploi")
if data:
participants.update(record_id, data)
return redirect(url_for('airtable.display_participant_form', participant_number=participant_number))
else:
return render_template('airtable/participants/participants_edit.html', record=record)
@airtable_bp.route('/my_modules', methods=['GET'])
@auth_required()
def display_records():
profs = fetch_api('profs')
all_modules = fetch_api('log_modules')
# Get the mentor's email from the current user
user_email = current_user.email
for prof in profs.get_all():
if prof['fields'].get('email') and prof['fields']['email'] == user_email:
print("prof found: " + prof['fields']['email'] + " - id : "+ prof['id'])
records = [record for record in all_modules.get_all() if record['fields'].get('Professeur') and record['fields']['Professeur'][0] == prof['id']]
break
else:
records = []
return render_template('my_modules.html', records=records, profs=profs)
@airtable_bp.route('/edit_record/<record_id>', methods=['GET', 'POST'])
def edit_record(record_id):
field_options = {}
for field, table_name in ext_fields.items():
field_options[field.capitalize()] = get_field_options(base_id, table_name)
record = airtable.get(record_id)
if request.method == 'POST':
data = {}
if request.form['name'] != record['fields']['Name']:
data['Name'] = request.form['name']
if request.form['notes'] != record['fields']['Notes']:
data['Notes'] = request.form['notes']
if request.form['status'] != record['fields']['Status']:
data['Status'] = request.form['status']
if request.form['atelier'] != record['fields']['atelier'][0]:
data['atelier'] = [request.form['atelier']]
if set(request.form.getlist('mentors')) != set(record['fields'].get('mentors', [])):
data['mentors'] = request.form.getlist('mentors')
if data:
airtable.update(record_id, data)
return redirect(url_for('airtable.display_records'))
else:
record = airtable.get(record_id)
return render_template('edit_record.html', record=record, field_options=field_options)
@airtable_bp.route('/create_record', methods=['GET', 'POST'])
def create_record():
field_options = {}
for field, table_name in ext_fields.items():
field_options[field.capitalize()] = get_field_options(base_id, table_name)
if request.method == 'POST':
data = {
'Name': request.form['name'],
'Notes': request.form['notes'],
'Status': request.form['status'],
'atelier': [request.form['atelier']],
'mentors': [request.form['mentors']]
#'Date et heure de la fin du Passe Numérique': request.form['date_time'],
#'As-tu terminé le Passe Numérique jusqu\'au bout?': request.form['completed'],
#'Résultat à la sortie de l\'opération': request.form['result'],
#'Choisi le niveau que tu penses avoir atteint à la fin du Passe Numérique': request.form['level'],
#'Peux-tu me parler de ton parcours?': request.form['story']
}
airtable.insert(data)
return redirect(url_for('airtable.submitted'))
return render_template('form.html', field_options=field_options)
@airtable_bp.route('/submitted')
def submitted():
return render_template('submitted.html')

156
project/app.py Normal file
View File

@ -0,0 +1,156 @@
import os
import sys
import string
import random
import uuid
from flask import Flask, session, render_template_string, render_template, redirect, url_for, flash, request, current_app
from flask_security import Security, current_user, auth_required, hash_password, SQLAlchemySessionUserDatastore
from flask_login import login_user, logout_user
from flask_font_awesome import FontAwesome
from flask_dance.contrib.google import make_google_blueprint, google
from urllib.parse import urlencode
from flask_mailman import Mail
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.security import generate_password_hash
from .config import Config, ProductionConfig, DevelopmentConfig
from dotenv import load_dotenv
from .database import db_session, init_db
from .models import User, Role
from .views import views_bp
from .airtableget import airtable_bp
fa = FontAwesome()
# Create app
def create_app():
app = Flask(__name__, instance_relative_config=True)
app.config['MAX_CONTENT_LENGTH'] = 20 * 1024 * 1024 # Set the maximum allowed request size to 100MB
debug_mode = False
print("Creating app")
for i, arg in enumerate(sys.argv):
print("arg: " + str(arg))
if arg == "--debug":
print("debug mode activé")
app.config.from_object(DevelopmentConfig)
debug_mode = True
break
print("end of variable retrieval")
if not debug_mode:
app.config.from_object(ProductionConfig)
# Create Google OAuth blueprint
google_blueprint = make_google_blueprint(
client_id=app.config['GOOGLE_OAUTH_CLIENT_ID'],
client_secret=app.config['GOOGLE_OAUTH_CLIENT_SECRET'],
scope=["https://www.googleapis.com/auth/userinfo.email", "openid"],
redirect_url="/authorized",
)
# Register the Google OAuth blueprint
app.register_blueprint(google_blueprint, url_prefix="/login")
def google_login_url():
google_auth_url = url_for("google.login", _external=True)
params = {
"access_type": "offline",
"prompt": "consent",
}
return f"{google_auth_url}?{urlencode(params)}"
@app.context_processor
def inject_google_login_url():
return dict(google_login_url=google_login_url)
user_datastore = SQLAlchemySessionUserDatastore(db_session, User, Role)
print("user_datastore retrieved in prod")
app.security = Security(app, user_datastore)
print("app.security initialized in prod")
mail = Mail(app)
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
@app.route('/authorized')
def authorized():
if debug_mode:
return "Not available in development mode", 404
if not google.authorized:
return redirect(url_for("google.login"))
resp = google.get("/oauth2/v3/tokeninfo")
if not resp.ok:
flash("Failed to authenticate with Google.", "danger")
return redirect(url_for("views.home"))
idinfo = resp.json()
email = idinfo['email']
if 'name' in idinfo:
name = idinfo['name']
else:
name = email
# Check if the user's email is in the allowed list
if email not in app.config['ALLOWED_USERS']:
flash("Your email is not authorized.", "danger")
return redirect(url_for("views.home"))
# Check if the user exists in the database
user = app.security.datastore.find_user(email=email)
# If the user does not exist, create a new user
if not user:
random_password = generate_random_password()
hashed_password = generate_password_hash(random_password, method='sha256')
#fs_uniquifier = str(uuid.uuid4())
user = app.security.datastore.create_user(email=email, username=name, password=hashed_password)
db_session.commit()
login_user(user)
# Set the session variable
session['logged_in_with_google'] = True
# Redirect to the desired page after login, e.g., the index page
return redirect(url_for("views.home"))
# Register the Google authorized route only in production
app.add_url_rule('/authorized', view_func=authorized)
def generate_random_password(length=12):
characters = string.ascii_letters + string.digits + string.punctuation
return ''.join(random.choice(characters) for i in range(length))
@app.route("/logout")
@auth_required()
def logout():
logout_user()
return redirect(url_for("views.home"))
app.register_blueprint(views_bp)
app.register_blueprint(airtable_bp)
with app.app_context():
# Create a user to test with
init_db()
for user in app.config['ALLOWED_USERS']:
if not (user.endswith("gmail.com") or user.endswith("thargo.io")) and not app.security.datastore.find_user(email=user):
app.security.datastore.create_user(email=user, password=hash_password(generate_random_password()))
db_session.commit()
#from admin import admin
#admin.init_app(app)
fa.init_app(app)
return app
app = create_app()
if __name__ == "__main__":
app.run()

45
project/config.py Normal file
View File

@ -0,0 +1,45 @@
import os
from dotenv import load_dotenv
import json
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(os.path.dirname(basedir), '.env'))
class Config():
DEBUG = False
TESTING = False
CSRF_ENABLED = True
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'None'
MAIL_SERVER = os.environ.get("MAIL_SERVER")
MAIL_PORT = os.environ.get("MAIL_PORT")
MAIL_USE_TLS = os.environ.get("MAIL_USE_TLS")
MAIL_USE_SSL = os.environ.get("MAIL_USE_SSL")
MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
MAIL_DEFAULT_SENDER = os.environ.get("MAIL_DEFAULT_SENDER")
SECURITY_RECOVERABLE = True
SECURITY_EMAIL_SENDER = os.environ.get("MAIL_SENDER")
class ProductionConfig(Config):
DEBUG = False
FLASK_DEBUG = False
SECRET_KEY = os.environ.get("PRODUCTION_SECRET_KEY")
SECURITY_PASSWORD_SALT = os.environ.get("PRODUCTION_SECURITY_PASSWORD_SALT")
SQLALCHEMY_TRACK_MODIFICATIONS = False
GOOGLE_OAUTH_CLIENT_ID = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
ALLOWED_USERS = json.loads(os.environ.get("ALLOWED_USERS"))
class DevelopmentConfig(Config):
DEVELOPMENT = True
FLASK_DEBUG = True
DEBUG = True
OAUTHLIB_INSECURE_TRANSPORT = True
SECRET_KEY = os.environ.get("DEV_SECRET_KEY")
SECURITY_PASSWORD_SALT = os.environ.get("DEV_SECURITY_PASSWORD_SALT")
SQLALCHEMY_TRACK_MODIFICATIONS = True
ALLOWED_USERS = json.loads(os.environ.get("ALLOWED_USERS"))

25
project/database.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from flask import current_app
import os
engine = None
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
def init_db():
DATABASE_URL = current_app.config['SQLALCHEMY_DATABASE_URI']
print("inside init_db, database_url : " + str(DATABASE_URL))
engine = create_engine(DATABASE_URL, pool_size=10, max_overflow=20)
db_session.configure(bind=engine)
# import all modules here that might define models so that
# they will be registered properly on the metadata. Otherwise
# you will have to import them first before calling init_db()
from . import models
Base.metadata.create_all(bind=engine)

36
project/models.py Normal file
View File

@ -0,0 +1,36 @@
from .database import Base
from flask_security import UserMixin, RoleMixin
from sqlalchemy import create_engine
from sqlalchemy.orm import relationship, backref
from sqlalchemy import Boolean, DateTime, Column, Integer, \
String, ForeignKey, UnicodeText
class RolesUsers(Base):
__tablename__ = 'roles_users'
id = Column(Integer(), primary_key=True)
user_id = Column('user_id', Integer(), ForeignKey('user.id'))
role_id = Column('role_id', Integer(), ForeignKey('role.id'))
class Role(Base, RoleMixin):
__tablename__ = 'role'
id = Column(Integer(), primary_key=True)
name = Column(String(80), unique=True)
description = Column(String(255))
permissions = Column(UnicodeText)
class User(Base, UserMixin):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
email = Column(String(255), unique=True)
username = Column(String(255), unique=True, nullable=True)
password = Column(String(255), nullable=False)
last_login_at = Column(DateTime())
current_login_at = Column(DateTime())
last_login_ip = Column(String(100))
current_login_ip = Column(String(100))
login_count = Column(Integer)
active = Column(Boolean())
fs_uniquifier = Column(String(255), unique=True, nullable=False)
confirmed_at = Column(DateTime())
roles = relationship('Role', secondary='roles_users',
backref=backref('users', lazy='dynamic'))

View File

@ -0,0 +1,5 @@
.thumbnail {
width: 100%;
height: auto;
object-fit: cover;
}

View File

@ -0,0 +1,147 @@
let oldImages = {};
let newImages = {};
function toggleThumbnail(attachmentId, fieldName) {
const redCross = document.getElementById(`red_cross_${fieldName}_${attachmentId}`);
const removeLabel = document.getElementById(`remove_label_${fieldName}_${attachmentId}`);
const inputElement = document.getElementById(`${fieldName}_${attachmentId}`);
if (redCross.style.display === "none") {
redCross.style.display = "block";
removeLabel.innerText = "Keep";
inputElement.disabled = true;
} else {
redCross.style.display = "none";
removeLabel.innerText = "Remove";
inputElement.disabled = false;
};
};
function removeNewThumbnail(inputId, fieldName) {
const columnElement = document.getElementById(`column_${inputId}`);
columnElement.remove();
};
function addImage(fieldName, columnsId) {
const columns = document.getElementById(columnsId);
const inputId = `${fieldName}_${columns.childElementCount + 1}`;
const thumbnailId = `new_${fieldName}_thumbnail_${columns.childElementCount + 1}`;
const removeLabelId = `remove_label_${fieldName}_${columns.childElementCount + 1}`;
// Create a new column element
const newColumn = document.createElement('div');
newColumn.setAttribute('class', 'column is-one-quarter');
newColumn.setAttribute('id', `column_${inputId}`);
// Create a new card element
const newCard = document.createElement('div');
newCard.setAttribute('class', 'card');
// Create a new card image element
const newCardImage = document.createElement('div');
newCardImage.setAttribute('class', 'card-image');
// Create a new figure element
const newFigure = document.createElement('figure');
newFigure.setAttribute('class', 'image is-4by3');
// Create a new thumbnail element
const newThumbnail = document.createElement('img');
newThumbnail.setAttribute('class', 'thumbnail');
newThumbnail.setAttribute('id', thumbnailId);
// Create new thumbnail link
const newAThumbnail = document.createElement('a');
newAThumbnail.setAttribute('href', '#');
newAThumbnail.setAttribute('target', '_blank');
newAThumbnail.appendChild(newThumbnail);
// Add elements to the figure
newFigure.appendChild(newAThumbnail);
// Add elements to the card image
newCardImage.appendChild(newFigure);
// Create a new card footer element
const newCardFooter = document.createElement('div');
newCardFooter.setAttribute('class', 'card-footer');
const newRemoveLabelDiv = document.createElement('div');
newRemoveLabelDiv.setAttribute('class', 'button card-footer-item');
newRemoveLabelDiv.setAttribute('id', removeLabelId);
newRemoveLabelDiv.setAttribute('data-attachment-id', inputId);
newRemoveLabelDiv.innerText = 'Remove';
// Add elements to the card footer
newCardFooter.appendChild(newRemoveLabelDiv);
// Add elements to the card
newCard.appendChild(newCardImage);
newCard.appendChild(newCardFooter);
// Add elements to the new column
newColumn.appendChild(newCard);
// Append the new column element to the columns container
columns.appendChild(newColumn);
// Trigger the "click" event on the file input
const newInput = document.createElement('input');
newInput.setAttribute('id', `${inputId}`);
newInput.setAttribute('type', 'file');
newInput.setAttribute('data-field', `${fieldName}`)
newInput.style.display = 'none';
newInput.setAttribute('name', `${inputId}`);
newCard.appendChild(newInput);
newInput.click();
window.addEventListener('focus', function handleFocus() {
window.removeEventListener('focus', handleFocus); // Remove the event listener to prevent it from triggering multiple times
setTimeout(() => {
const file = newInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
// Set the thumbnail source
newThumbnail.src = e.target.result;
newAThumbnail.href = e.target.result;
// Add a click event listener to the remove label
newRemoveLabelDiv.addEventListener('click', function (event) {
event.preventDefault();
removeNewThumbnail(inputId, fieldName);
});
};
reader.readAsDataURL(file);
} else {
// Remove the div and input if the file dialog is canceled
removeNewThumbnail(inputId);
}
// Remove the input element after processing the file
//newInput.remove();
}, 100); // Add a 100ms delay
});
};
// Add a click event listener for each add-image-label element
document.querySelectorAll('[id^="add-image-label"]').forEach(function (element) {
element.addEventListener('click', function (event) {
event.preventDefault();
const fieldName = element.getAttribute('data-field');
const columnsContainer = element.getAttribute('data-columns-container');
addImage(fieldName, columnsContainer);
});
});
// Add event listeners for existing images fetched from Airtable
document.querySelectorAll('.existing-remove-label').forEach(function (removeLabel) {
const fieldName = removeLabel.getAttribute('data-field');
const attachmentId = removeLabel.getAttribute('data-attachment-id');
removeLabel.addEventListener('click', function (event) {
event.preventDefault();
toggleThumbnail(attachmentId, fieldName);
});
});

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" id="full-cross-circle-alt" data-name="Line Color" xmlns="http://www.w3.org/2000/svg" class="icon line-color"><line id="secondary" x1="18.15" y1="5.85" x2="5.85" y2="18.15" style="fill: none; stroke: rgb(44, 169, 188); stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></line><circle id="primary" cx="12" cy="12" r="9" style="fill: none; stroke: rgb(0, 0, 0); stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></circle></svg>

After

Width:  |  Height:  |  Size: 642 B

View File

@ -0,0 +1,43 @@
{% import 'admin/static.html' as admin_static with context %}
{% macro dropdown(actions) -%}
<div class="dropdown is-active is-overlay">
<a class="dropdown-trigger" aria-controls="dropdown-menu" role="button" aria-haspopup="true"
aria-expanded="false">
<span>{{ _gettext('With selected') }}</span>
<span class="icon is-small">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
{% for p in actions %}
<a class="dropdown-item" href="javascript:void(0)"
onclick="return modelActions.execute('{{ p[0] }}');">{{ _gettext(p[1]) }}</a>
{% endfor %}
</div>
</div>
</div>
{% endmacro %}
{% macro form(actions, url) %}
{% if actions %}
<form id="action_form" action="{{ url }}" method="POST" style="display: none">
{% if action_form.csrf_token %}
{{ action_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
{{ action_form.url(value=return_url) }}
{{ action_form.action() }}
</form>
{% endif %}
{% endmacro %}
{% macro script(message, actions, actions_confirmation) %}
{% if actions %}
<div id="actions-confirmation-data" style="display:none;">{{ actions_confirmation|tojson|safe }}</div>
<div id="message-data" style="display:none;">{{ message|tojson|safe }}</div>
<script src="{{ admin_static.url(filename='admin/js/actions.js', v='1.0.0') }}"></script>
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,9 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% block content %}
{% block header %}<h3>{{ header_text }}</h3>{% endblock %}
{% block fa_form %}
{{ lib.render_form(form, dir_url) }}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,191 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% import 'admin/actions.html' as actionslib with context %}
{% block content %}
{% block breadcrums %}
<nav area-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ get_dir_url('.index_view', path=None) }}">{{ _gettext('Root') }}</a>
</li>
{% for name, path in breadcrumbs[:-1] %}
<li class="breadcrumb-item">
<a href="{{ get_dir_url('.index_view', path=path) }}">{{ name }}</a>
</li>
{% endfor %}
{% if breadcrumbs %}
<li class="breadcrumb-item">
<a href="{{ get_dir_url('.index_view', path=breadcrumbs[-1][1]) }}">{{ breadcrumbs[-1][0] }}</a>
</li>
{% endif %}
</ol>
</nav>
{% endblock %}
{% block file_list_table %}
<div class="table-responsive">
<table class="table table-striped table-bordered model-list">
<thead>
<tr>
{% block list_header scoped %}
{% if actions %}
<th class="list-checkbox-column">
<input type="checkbox" name="rowtoggle" class="action-rowtoggle" />
</th>
{% endif %}
<th class="">&nbsp;</th>
{% for column in admin_view.column_list %}
<th>
{% if admin_view.is_column_sortable(column) %}
{% if sort_column == column %}
<a href="{{ sort_url(column, dir_path, True) }}" title="{{ _gettext('Sort by %(name)s', name=column) }}">
{{ admin_view.column_label(column) }}
{% if sort_desc %}
<span class="fa fa-chevron-up glyphicon glyphicon-chevron-up"></span>
{% else %}
<span class="fa fa-chevron-down glyphicon glyphicon-chevron-down"></span>
{% endif %}
</a>
{% else %}
<a href="{{ sort_url(column, dir_path) }}" title="{{ _gettext('Sort by %(name)s', name=column) }}">{{ admin_view.column_label(column) }}</a>
{% endif %}
{% else %}
{{ _gettext(admin_view.column_label(column)) }}
{% endif %}
</th>
{% endfor %}
{% endblock %}
</tr>
</thead>
{% for name, path, is_dir, size, date in items %}
<tr>
{% block list_row scoped %}
{% if actions %}
<td>
{% if not is_dir %}
<input type="checkbox" name="rowid" class="action-checkbox" value="{{ path }}" />
{% endif %}
</td>
{% endif %}
<td>
{% block list_row_actions scoped %}
{% if admin_view.can_rename and path and name != '..' %}
{%- if admin_view.rename_modal -%}
{{ lib.add_modal_button(url=get_url('.rename', path=path, modal=True),
title=_gettext('Rename File'),
content='<i class="fa fa-pencil glyphicon glyphicon-pencil"></i>') }}
{% else %}
<a class="icon" href="{{ get_url('.rename', path=path) }}" title="{{ _gettext('Rename File') }}">
<i class="fa fa-pencil glyphicon glyphicon-pencil"></i>
</a>
{%- endif -%}
{% endif %}
{%- if admin_view.can_delete and path -%}
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="fa fa-times glyphicon glyphicon-remove"></i>
</button>
</form>
{% endif %}
{% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}">
{{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="fa fa-trash glyphicon glyphicon-trash"></i>
</button>
</form>
{% endif %}
{%- endif -%}
{% endblock %}
</td>
{% if is_dir %}
<td colspan="2">
<a href="{{ get_dir_url('.index_view', path)|safe }}">
<i class="fa fa-folder-o glyphicon glyphicon-folder-close"></i> <span>{{ name }}</span>
</a>
</td>
{% else %}
<td>
{% if admin_view.can_download %}
{%- if admin_view.edit_modal and admin_view.is_file_editable(path) -%}
{{ lib.add_modal_button(url=get_file_url(path, modal=True)|safe,
btn_class='', content=name) }}
{% else %}
<a href="{{ get_file_url(path)|safe }}">{{ name }}</a>
{%- endif -%}
{% else %}
{{ name }}
{% endif %}
</td>
{% if admin_view.is_column_visible('size') %}
<td>
{{ size|filesizeformat }}
</td>
{% endif %}
{% endif %}
{% if admin_view.is_column_visible('date') %}
<td>
{{ timestamp_format(date) }}
</td>
{% endif %}
{% endblock %}
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
{% block toolbar %}
<div class="btn-toolbar">
{% if admin_view.can_upload %}
<div class="btn-group">
{%- if admin_view.upload_modal -%}
{{ lib.add_modal_button(url=get_dir_url('.upload', path=dir_path, modal=True),
btn_class="btn btn-secondary",
content=_gettext('Upload File')) }}
{% else %}
<a class="btn btn-secondary" href="{{ get_dir_url('.upload', path=dir_path) }}">{{ _gettext('Upload File') }}</a>
{%- endif -%}
</div>
{% endif %}
{% if admin_view.can_mkdir %}
<div class="mx-1">
{%- if admin_view.mkdir_modal -%}
{{ lib.add_modal_button(url=get_dir_url('.mkdir', path=dir_path, modal=True),
btn_class="btn btn-secondary",
content=_gettext('Create Directory')) }}
{% else %}
<a class="btn btn-secondary" href="{{ get_dir_url('.mkdir', path=dir_path) }}">{{ _gettext('Create Directory') }}</a>
{%- endif -%}
</div>
{% endif %}
{% if actions %}
<div class="mx-1">
{{ actionslib.dropdown(actions, 'dropdown-toggle btn btn-secondary') }}
</div>
{% endif %}
</div>
{% endblock %}
{% block actions %}
{{ actionslib.form(actions, get_url('.action_view')) }}
{% endblock %}
{%- if admin_view.rename_modal or admin_view.mkdir_modal
or admin_view.upload_modal or admin_view.edit_modal -%}
{{ lib.add_modal_window() }}
{%- endif -%}
{% endblock %}
{% block tail %}
{{ super() }}
{{ actionslib.script(_gettext('Please select at least one file.'),
actions,
actions_confirmation) }}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% import 'admin/static.html' as admin_static with context %}
{% import 'admin/lib.html' as lib with context %}
{% block body %}
{# content added to modal-content #}
<div class="modal-header">
{% block header %}<h3>{{ header_text }}</h3>{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{% block fa_form %}
{{ lib.render_form(form, dir_url, action=request.url, is_modal=True) }}
{% endblock %}
</div>
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends 'admin/master.html' %}
{% block content %}
Bonjour
{% endblock %}

View File

@ -0,0 +1,294 @@
{% import 'admin/static.html' as admin_static with context %}
{# ---------------------- Pager -------------------------- #}
{% macro pager(page, pages, generator) -%}
{% if pages > 1 %}
<ul class="pagination">
{% set min = page - 3 %}
{% set max = page + 3 + 1 %}
{% if min < 0 %}
{% set max = max - min %}
{% endif %}
{% if max >= pages %}
{% set min = min - max + pages %}
{% endif %}
{% if min < 0 %}
{% set min = 0 %}
{% endif %}
{% if max >= pages %}
{% set max = pages %}
{% endif %}
{% if min > 0 %}
<li class="page-item">
<a class="page-link" href="{{ generator(0) }}">&laquo;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&laquo;</a>
</li>
{% endif %}
{% if page > 0 %}
<li class="page-item">
<a class="page-link" href="{{ generator(page-1) }}">&lt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&lt;</a>
</li>
{% endif %}
{% for p in range(min, max) %}
{% if page == p %}
<li class="page-item active">
<a class="page-link" href="javascript:void(0)">{{ p + 1 }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ generator(p) }}">{{ p + 1 }}</a>
</li>
{% endif %}
{% endfor %}
{% if page + 1 < pages %}
<li class="page-item">
<a class="page-link" href="{{ generator(page + 1) }}">&gt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&gt;</a>
</li>
{% endif %}
{% if max < pages %}
<li class="page-item">
<a class="page-link" href="{{ generator(pages - 1) }}">&raquo;</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="javascript:void(0)">&raquo;</a>
</li>
{% endif %}
</ul>
{% endif %}
{%- endmacro %}
{% macro simple_pager(page, have_next, generator) -%}
<ul class="pagination">
{% if page > 0 %}
<li class="page-item">
<a href="{{ generator(page - 1) }}">&lt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="{{ generator(0) }}">&lt;</a>
</li>
{% endif %}
{% if have_next %}
<li class="page-item">
<a href="{{ generator(page + 1) }}">&gt;</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="{{ generator(page) }}">&gt;</a>
</li>
{% endif %}
</ul>
{%- endmacro %}
{# ---------------------- Modal Window ------------------- #}
{% macro add_modal_window(modal_window_id='fa_modal_window', modal_label_id='fa_modal_label') %}
<div class="modal fade" id="{{ modal_window_id }}" tabindex="-1" role="dialog" aria-labelledby="{{ modal_label_id }}">
<div class="modal-dialog modal-xl" role="document">
{# bootstrap version > 3.1.0 required for this to work #}
<div class="modal-content">
</div>
</div>
</div>
{% endmacro %}
{% macro add_modal_button(url='', title='', content='', modal_window_id='fa_modal_window', btn_class='icon') %}
<a class="{{ btn_class }}" data-target="#{{ modal_window_id }}" title="{{ title }}" href="{{ url }}" data-toggle="modal">
{{ content|safe }}
</a>
{% endmacro %}
{# ---------------------- Forms -------------------------- #}
{% macro render_field(form, field, kwargs={}, caller=None) %}
{% set direct_error = h.is_field_error(field.errors) %}
{% set prepend = kwargs.pop('prepend', None) %}
{% set append = kwargs.pop('append', None) %}
<div class="form-group {{ kwargs.get('column_class', '') }}">
<label for="{{ field.id }}" class="control-label" {% if field.widget.input_type == 'checkbox' %}style="display: block; margin-bottom: 0"{% endif %}>{{ field.label.text }}
{% if h.is_required_form_field(field) %}
<strong style="color: red">&#42;</strong>
{%- else -%}
&nbsp;
{%- endif %}
</label>
{% if prepend or append %}
<div class="input-group">
{%- if prepend -%}
<div class="input-group-prepend">
{{ prepend }}
</div>
{%- endif -%}
{% endif %}
{% if field.widget.input_type == 'checkbox' %}
{% set _class = kwargs.setdefault('class', '') %}
{% elif field.widget.input_type == 'file' %}
{% set _class = kwargs.setdefault('class', 'form-control-file') %}
{% else %}
{% set _class = kwargs.setdefault('class', 'form-control') %}
{% endif %}
{%- if direct_error %} {% set _ = kwargs.update({'class': kwargs['class'] ~ ' is-invalid'}) %} {% endif -%}
{{ field(**kwargs) | safe }}
{%- if append -%}
<div class="input-group-append">
{{ append }}
</div>
{%- endif -%}
{% if direct_error %}
<div class="invalid-feedback">
<ul class="form-text text-muted" {% if field.widget.input_type == 'checkbox' %}style="margin-top: 0"{% endif %}>
{% for e in field.errors if e is string %}
<li>{{ e }}</li>
{% endfor %}
</ul>
</div>
{% elif field.description %}
<small class="form-text text-muted" {% if field.widget.input_type == 'checkbox' %}style="margin-top: 0"{% endif %}>
{{ field.description|safe }}
</small>
{% endif %}
{% if prepend or append %}
</div>
{% endif %}
{% if caller %}
{{ caller(form, field, direct_error, kwargs) }}
{% endif %}
</div>
{% endmacro %}
{% macro render_header(form, text) %}
<h3>{{ text }}</h3>
{% endmacro %}
{% macro render_form_fields(form, form_opts=None) %}
{% if form.hidden_tag is defined %}
{{ form.hidden_tag() }}
{% else %}
{% if csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
{% for f in form if f.widget.input_type == 'hidden' %}
{{ f }}
{% endfor %}
{% endif %}
{% if form_opts and form_opts.form_rules %}
{% for r in form_opts.form_rules %}
{{ r(form, form_opts=form_opts) }}
{% endfor %}
{% else %}
{% for f in form if f.widget.input_type != 'hidden' %}
{% if form_opts %}
{% set kwargs = form_opts.widget_args.get(f.short_name, {}) %}
{% else %}
{% set kwargs = {} %}
{% endif %}
{{ render_field(form, f, kwargs) }}
{% endfor %}
{% endif %}
{% endmacro %}
{% macro form_tag(form=None, action=None) %}
<form action="{{ action or '' }}" method="POST" role="form" class="admin-form" enctype="multipart/form-data">
<fieldset>
{{ caller() }}
</fieldset>
</form>
{% endmacro %}
{% macro render_form_buttons(cancel_url, extra=None, is_modal=False) %}
{% if is_modal %}
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}
{% else %}
<hr>
<div class="form-group">
<div class="col-md-offset-2 col-md-10 submit-row">
<input type="submit" class="btn btn-primary" value="{{ _gettext('Save') }}" />
{% if extra %}
{{ extra }}
{% endif %}
{% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button" {% if is_modal %}data-dismiss="modal"{% endif %}>{{ _gettext('Cancel') }}</a>
{% endif %}
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_form(form, cancel_url, extra=None, form_opts=None, action=None, is_modal=False) -%}
{% call form_tag(action=action) %}
{{ render_form_fields(form, form_opts=form_opts) }}
{{ render_form_buttons(cancel_url, extra, is_modal) }}
{% endcall %}
{% endmacro %}
{% macro form_css() %}
<link href="{{ admin_static.url(filename='vendor/select2/select2.css', v='4.2.1') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/select2/select2-bootstrap4.css', v='1.4.6') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs4.css', v='1.3.22') }}" rel="stylesheet">
{% if config.MAPBOX_MAP_ID %}
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css', v='1.0.2') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css', v='0.4.6') }}" rel="stylesheet">
{% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap4-editable.css', v='1.5.1.1') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
{% if config.MAPBOX_MAP_ID %}
<script>
window.MAPBOX_MAP_ID = "{{ config.MAPBOX_MAP_ID }}";
{% if config.MAPBOX_ACCESS_TOKEN %}
window.MAPBOX_ACCESS_TOKEN = "{{ config.MAPBOX_ACCESS_TOKEN }}";
{% endif %}
{% if config.DEFAULT_CENTER_LAT and config.DEFAULT_CENTER_LONG %}
window.DEFAULT_CENTER_LAT = "{{ config.DEFAULT_CENTER_LAT }}";
window.DEFAULT_CENTER_LONG = "{{ config.DEFAULT_CENTER_LONG }}";
{% endif %}
</script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.js', v='1.0.2') }}"></script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js', v='0.4.6') }}"></script>
{% if config.MAPBOX_SEARCH %}
<script>
window.MAPBOX_SEARCH = "{{ config.MAPBOX_SEARCH }}";
</script>
<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=places&key={{ config.get('GOOGLE_MAPS_API_KEY') }}"></script>
{% endif %}
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js', v='1.3.22') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap4-editable.min.js', v='1.5.1.1') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='admin/js/form.js', v='1.0.1') }}"></script>
{% endmacro %}
{% macro extra() %}
{% if admin_view.can_create %}
<input name="_add_another" type="submit" class="btn btn-secondary" value="{{ _gettext('Save and Add Another') }}" />
{% endif %}
{% if admin_view.can_edit %}
<input name="_continue_editing" type="submit" class="btn btn-secondary" value="{{ _gettext('Save and Continue Editing') }}" />
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,72 @@
{% import 'admin/layout.html' as layout with context -%}
{% import 'admin/static.html' as admin_static with context %}
{% extends "/base.html" %}
{% block head_scripts %}
{% block head_admin_scripts %}
{% endblock %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/swatch/{swatch}/bootstrap.min.css'.format(swatch=config.get('FLASK_ADMIN_SWATCH', 'default')), v='4.2.1') }}"
rel="stylesheet">
{% if config.get('FLASK_ADMIN_SWATCH', 'default') == 'default' %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/css/bootstrap.min.css', v='4.2.1') }}" rel="stylesheet">
{% endif %}
<link href="{{ admin_static.url(filename='admin/css/bootstrap4/admin.css', v='1.1.1') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='bootstrap/bootstrap4/css/font-awesome.min.css', v='4.7.0') }}" rel="stylesheet">
{% if admin_view.extra_css %}
{% for css_url in admin_view.extra_css %}
<link href="{{ css_url }}" rel="stylesheet">
{% endfor %}
{% endif %}
{{ super() }}
{% endblock %}
{%- block styles %}
{{ super() }}
{% endblock %}
{% block page_body %}
{% block brand %}
{% endblock %}
{% block main_menu %}
{% endblock %}
{% block menu_links %}
{% endblock %}
{% block access_control %}
{% endblock %}
{% block messages %}
{% endblock %}
{# store the jinja2 context for form_rules rendering logic #}
{% set render_ctx = h.resolve_ctx() %}
{% endblock %}
{% block scripts %}
{{ super() }}
{% block tail_js %}
<script src="{{ admin_static.url(filename='vendor/jquery.min.js', v='3.5.1') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='bootstrap/bootstrap4/js/popper.min.js') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='bootstrap/bootstrap4/js/bootstrap.min.js', v='4.2.1') }}"
type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/bootstrap4/util.js', v='4.3.1') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/bootstrap4/dropdown.js', v='4.3.1') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='4.2.1') }}"
type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/multi-level-dropdowns-bootstrap/bootstrap4-dropdown-ml-hack.js') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='admin/js/helpers.js', v='1.0.0') }}" type="text/javascript"></script>
{% if admin_view.extra_js %}
{% for js_url in admin_view.extra_js %}
<script src="{{ js_url }}" type="text/javascript"></script>
{% endfor %}
{% endif %}
{% endblock tail_js %}
{% block tail %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
{% block head_admin_scripts %}
{{ super() }}
{{ lib.form_css() }}
{% endblock %}
{% block content %}
{% block navlinks %}
<div class="tabs is-boxed">
<ul>
<li>
<a href="{{ return_url }}">{{ _gettext('List') }}</a>
</li>
<li>
<a href="javascript:void(0)" class="is-active">{{ _gettext('Create') }}</a>
</li>
</ul>
</div>
{% endblock %}
{% block create_form %}
{{ lib.render_form(form, return_url, extra(), form_opts) }}
{% endblock %}
{% endblock %}
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% block content %}
{% block navlinks %}
<div class="tabs">
<ul>
<li>
<a href="{{ return_url }}">{{ _gettext('List') }}</a>
</li>
{%- if admin_view.can_create -%}
<li>
<a href="{{ get_url('.create_view', url=return_url) }}">{{ _gettext('Create') }}</a>
</li>
{%- endif -%}
{%- if admin_view.can_edit -%}
<li>
<a href="{{ get_url('.edit_view', id=request.args.get('id'), url=return_url) }}">{{ _gettext('Edit') }}</a>
</li>
{%- endif -%}
<li>
<a class="is-active disabled" href="javascript:void(0)">{{ _gettext('Details') }}</a>
</li>
</ul>
</div>
{% endblock %}
{% block details_search %}
<div class="form-inline fa_filter_container col-lg-6">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">
</div>
{% endblock %}
{% block details_table %}
<table class="table table-hover table-bordered searchable">
{% for c, name in details_columns %}
<tr>
<td>
<b>{{ name }}</b>
</td>
<td>
{{ get_value(model, c) }}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% endblock %}
{% block tail %}
{{ super() }}
<script src="{{ admin_static.url(filename='admin/js/details_filter.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
{% block head_admin_scripts %}
{{ super() }}
{{ lib.form_css() }}
{% endblock %}
{% block content %}
{% block navlinks %}
<div class="tabs is-boxed">
<ul>
<li>
<a href="{{ return_url }}">{{ _gettext('List') }}</a>
</li>
{%- if admin_view.can_create -%}
<li>
<a href="{{ get_url('.create_view', url=return_url) }}">{{ _gettext('Create') }}</a>
</li>
{%- endif -%}
<li>
<a href="javascript:void(0)" class="is-active">{{ _gettext('Edit') }}</a>
</li>
{%- if admin_view.can_view_details -%}
<li>
<a href="{{ get_url('.details_view', id=request.args.get('id'), url=return_url) }}">{{ _gettext('Details') }}</a>
</li>
{%- endif -%}
</ul>
</div>
{% endblock %}
{% block edit_form %}
{{ lib.render_form(form, return_url, extra(), form_opts) }}
{% endblock %}
{% endblock %}
{% block tail %}
{{ super() }}
{{ lib.form_js() }}
{% endblock %}

View File

@ -0,0 +1,212 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/model/layout.html' as model_layout with context %}
{% import 'admin/actions.html' as actionlib with context %}
{% import 'admin/model/row_actions.html' as row_actions with context %}
{% block head_admin_scripts %}
{{ super() }}
{{ lib.form_css() }}
{% endblock %}
{% block content %}
{% block model_menu_bar %}
<div class="tabs is-boxed">
<ul>
<li>
<a href="javascript:void(0)" class="is-active">{{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}</a>
</li>
{% if admin_view.can_create %}
<li>
{%- if admin_view.create_modal -%}
{{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), btn_class='nav-link', title=_gettext('Create New Record'), content=_gettext('Create')) }}
{% else %}
<a href="{{ get_url('.create_view', url=return_url) }}" title="{{ _gettext('Create New Record') }}" class="nav-link">{{ _gettext('Create') }}</a>
{%- endif -%}
</li>
{% endif %}
{% if admin_view.can_export %}
{{ model_layout.export_options() }}
{% endif %}
{% block model_menu_bar_before_filters %}{% endblock %}
{% if filters %}
<li class="dropdown">
{{ model_layout.filter_options() }}
</li>
{% endif %}
{% if can_set_page_size %}
<li class="dropdown">
{{ model_layout.page_size_form(page_size_url) }}
</li>
{% endif %}
{% if actions %}
<li>
{{ actionlib.dropdown(actions) }}
</li>
{% endif %}
{% if search_supported %}
<li class="box">
{{ model_layout.search_form() }}
</li>
{% endif %}
{% block model_menu_bar_after_filters %}{% endblock %}
</ul>
</div>
{% endblock %}
{% if filters %}
{{ model_layout.filter_form() }}
<div class=is-clearfix"></div>
{% endif %}
{% block model_list_table %}
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable is-fullwidth model-list">
<thead>
<tr>
{% block list_header scoped %}
{% if actions %}
<th class="has-text-centered">
<input type="checkbox" name="rowtoggle" title="{{ _gettext('Select all records') }}" />
</th>
{% endif %}
{% block list_row_actions_header %}
{% if admin_view.column_display_actions %}
<th class="has-text-centered">&nbsp;</th>
{% endif %}
{% endblock %}
{% for c, name in list_columns %}
{% set column = loop.index0 %}
<th class="has-text-centered">
{% if admin_view.is_sortable(c) %}
{% if sort_column == column %}
<a href="{{ sort_url(column, True) }}" title="{{ _gettext('Sort by %(name)s', name=name) }}">
{{ name }}
{% if sort_desc %}
<span class="icon fa fa-chevron-up glyphicon glyphicon-chevron-up"></span>
{% else %}
<span class="icon fa fa-chevron-down glyphicon glyphicon-chevron-down"></span>
{% endif %}
</a>
{% else %}
<a href="{{ sort_url(column) }}" title="{{ _gettext('Sort by %(name)s', name=name) }}">{{ name }}</a>
{% endif %}
{% else %}
{{ name }}
{% endif %}
{% if admin_view.column_descriptions.get(c) %}
<a class="icon fa fa-question-circle glyphicon glyphicon-question-sign"
title="{{ admin_view.column_descriptions[c] }}"
href="javascript:void(0)" data-role="tooltip"
></a>
{% endif %}
</th>
{% endfor %}
{% endblock %}
</tr>
</thead>
{% for row in data %}
<tr>
{% block list_row scoped %}
{% if actions %}
<td>
<input type="checkbox" name="rowid" value="{{ get_pk_value(row) }}" title="{{ _gettext('Select record') }}" />
</td>
{% endif %}
{% block list_row_actions_column scoped %}
{% if admin_view.column_display_actions %}
<td>
{% block list_row_actions scoped %}
{% for action in list_row_actions %}
{{ action.render_ctx(get_pk_value(row), row) }}
{% endfor %}
{% endblock %}
</td>
{%- endif -%}
{% endblock %}
{% for c, name in list_columns %}
<td>
{% if admin_view.is_editable(c) %}
{% set form = list_forms[get_pk_value(row)] %}
{% if form.csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
{% elif csrf_token %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=csrf_token()) }}
{% else %}
{{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
{% endif %}
{% else %}
{{ get_value(row, c) }}
{% endif %}
</td>
{% endfor %}
{% endblock %}
</tr>
{% else %}
<tr>
<td>
{% block empty_list_message %}
<div>
{{ admin_view.get_empty_list_message() }}
</div>
{% endblock %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% block list_pager %}
{% if num_pages is not none %}
{{ lib.pager(page, num_pages, pager_url) }}
{% else %}
{{ lib.simple_pager(page, data|length == page_size, pager_url) }}
{% endif %}
{% endblock %}
{% endblock %}
{% block actions %}
{{ actionlib.form(actions, get_url('.action_view')) }}
{% endblock %}
{%- if admin_view.edit_modal or admin_view.create_modal or admin_view.details_modal -%}
{{ lib.add_modal_window() }}
{%- endif -%}
{% endblock %}
{% block tail %}
{{ super() }}
{% if filter_groups %}
<div id="filter-groups-data" style="display:none;">{{ filter_groups|tojson|safe }}</div>
<div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
{% endif %}
{{ lib.form_js() }}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/bs4_filters.js', v='1.0.0') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'),
actions,
actions_confirmation) }}
<script>
document.addEventListener('DOMContentLoaded', function () {
var dropdown = document.querySelector('.dropdown');
dropdown.addEventListener('click', function(event) {
event.stopPropagation();
dropdown.classList.toggle('is-active');
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/lib.html' as lib with context %}
{# store the jinja2 context for form_rules rendering logic #}
{% set render_ctx = h.resolve_ctx() %}
{% block body %}
<div class="modal-header">
{% block header_text %}<h5 class="modal-title">{{ _gettext('Create New Record') }}</h5>{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% call lib.form_tag(action=url_for('.create_view', url=return_url)) %}
<div class="modal-body">
{{ lib.render_form_fields(form, form_opts=form_opts) }}
</div>
<div class="modal-footer">
{{ lib.render_form_buttons(return_url, extra=None, is_modal=True) }}
</div>
{% endcall %}
{# "save and add" button is removed from modal (it won't function properly) #}
{# % block create_form %}
{{ lib.render_form(form, return_url, extra=None, form_opts=form_opts,
action=url_for('.create_view', url=return_url),
is_modal=True) }}
{% endblock % #}
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/lib.html' as lib with context %}
{% block body %}
<div class="modal-header">
{% block header_text %}
<h3>{{ _gettext('View Record') + ' #' + request.args.get('id') }}</h3>
{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{% block details_search %}
<div class="form-inline fa_filter_container col-lg-6">
<label for="fa_filter">{{ _gettext('Filter') }}</label>
<input id="fa_filter" type="text" class="ml-3 form-control">
</div>
{% endblock %}
{% block details_table %}
<table class="table table-hover table-bordered searchable">
{% for c, name in details_columns %}
<tr>
<td>
<b>{{ name }}</b>
</td>
<td>
{{ get_value(model, c) }}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
</div>
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/details_filter.js', v='1.0.0') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% import 'admin/static.html' as admin_static with context%}
{% import 'admin/lib.html' as lib with context %}
{# store the jinja2 context for form_rules rendering logic #}
{% set render_ctx = h.resolve_ctx() %}
{% block body %}
<div class="modal-header">
{% block header_text %}
<h5 class="modal-title">{{ _gettext('Edit Record') + ' #' + request.args.get('id') }}</h5>
{% endblock %}
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% call lib.form_tag(action=url_for('.edit_view', id=request.args.get('id'), url=return_url)) %}
<div class="modal-body">
{{ lib.render_form_fields(form, form_opts=form_opts) }}
</div>
<div class="modal-footer">
{{ lib.render_form_buttons(return_url, extra=None, is_modal=True) }}
</div>
{% endcall %}
{% endblock %}
{% block tail %}
<script src="{{ admin_static.url(filename='admin/js/bs4_modal.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib with context %}
{% import 'admin/static.html' as admin_static with context%}
{%- block head_scripts %}
{{ super() }}
<link href="{{ admin_static.url(filename='admin/css/bootstrap4/rediscli.css', v='1.0.0') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="console">
<div class="console-container">
</div>
<div class="console-line mb-4">
<form action="#">
<input type="text"></input>
</form>
</div>
</div>
{% endblock %}
{% block tail %}
{{ super() }}
<div id="execute-view-data" style="display:none;">{{ admin_view.get_url('.execute_view')|tojson|safe }}</div>
<script src="{{ admin_static.url(filename='admin/js/rediscli.js', v='1.0.0') }}"></script>
{% endblock %}

View File

@ -0,0 +1,59 @@
{% extends 'base.html' %}
{%- block head_scripts %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/thumbnail.css') }}"/>
{%- endblock head_scripts %}
{% block styles %}
{{ super() }}
<style>
.loader-container {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
z-index: 9999;
}
.loader {
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock styles %}
{% block content %}
{% endblock %}
{% block footer -%}
{% include "airtable/partials/footer.html" %}
{%- endblock footer %}
{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/attachment.js') }}"></script>
{% endblock scripts %}

View File

@ -0,0 +1,8 @@
{% extends 'airtable_base.html' %}
{% block content %}
{{ super() }}
<div class="column">
<h2 class="title">Tableau de bord</h1>
</div>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends 'airtable_base.html' %}
{% block content %}
{{ super() }}
<h2>Edit Inscription Record</h2>
<form method="POST">
<label for="name">Name</label>
<input id="name" name="name" value="{{ record.fields.Name }}" required><br><br>
<label for="notes">Notes</label>
<textarea id="notes" name="notes">{{ record.fields.Notes }}</textarea><br><br>
<label for="status">Status</label>
<select id="status" name="status">
{% set status_options = ['Todo', 'In progress', 'Done'] %}
{% for option in status_options %}
<option value="{{ option }}" {% if option == record['fields']['Status'] %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select><br><br>
<label for="atelier">Atelier</label>
<select id="atelier" name="atelier">
{% for option in field_options['Atelier'] %}
<option value="{{ option }}" {% if option == record['fields']['atelier'][0] %}selected{% endif %}>{{ field_options['Atelier'][option].get('Name') }}</option>
{% endfor %}
</select><br><br>
<label for="mentors">Mentors</label>
<div class="control">
<div class="select is-multiple">
<select id="mentors" name="mentors" multiple>
{% for option in field_options['Mentors'] %}
<option value="{{ option }}" {% if option in record['fields']['mentors'] %}selected{% endif %}>{{ field_options['Mentors'][option].get('Name') }}</option>
{% endfor %}
</select>
</div>
</div><br><br>
<input type="submit" value="Submit">
</form>
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "airtable_base.html" %}
{% block content %}
{{ super() }}
<h1>Questionnaire final de sortie du Passe Numérique</h1>
<form action="{{ url_for('airtable.create_record') }}" method="POST">
<label for="name">Prénom et Nom *</label>
<input type="text" id="name" name="name" required><br><br>
<label for="name">Notes</label>
<input type="text" id="notes" name="notes" required><br><br>
<label for="status">Status</label>
<select id="status" name="status">
<option>Todo</option>
<option>In progress</option>
<option>Done</option>
</select><br><br>
<label for="atelier">Atelier</label>
<select id="atelier" name="atelier">
{% for option in field_options['Atelier'] %}
<option value="{{ option }}">{{ field_options['Atelier'][option].get('Name') }}</option>
{% endfor %}
</select><br><br>
<label for="mentors">Mentors</label>
<div class="control">
<div class="select is-multiple">
<select id="mentors" name="mentors" multiple>
{% for option in field_options['Mentors'] %}
<option value="{{ option.id }}" {% if option.id in record['fields']['mentors'] %}selected{% endif %}>{{ option.fields['Name'] }}</option>
{% endfor %}
</select>
</div>
</div><br><br>
<label for="completed">As-tu terminé le Passe Numérique jusqu'au bout ? *</label>
<input type="text" id="completed" name="completed" required><br><br>
<label for="result">Résultat à la sortie de l'opération *</label>
<select id="result" name="result">
<option>result 1</option>
<option>result 2</option>
</select><br><br>
<label for="level">Choisi le niveau que tu penses avoir atteint à la fin du Passe Numérique : *</label>
<select id="level" name="level">
<option>level 1</option>
<option>level 2</option>
</select><br><br>
<label for="story">Peux-tu me parler de ton parcours ?</label>
<textarea id="story" name="story"></textarea><br><br>
<input type="submit" value="Submit">
</form>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends 'airtable_base.html' %}
{% block content %}
{{ super() }}
<div class="column is-9">
<section class="section">
<div class="container">
<h2 class="title">Suivi des modules</h2>
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Nom</th>
<th>Matière</th>
<th>Spécialité</th>
<th>Formation</th>
<th>Professeur</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr>
<td>{{ record['fields']['Name'] }}</td>
<td>{{ record['fields']['Matière'] }}</td>
<td>{{ record['fields']['Spécialité'] }}</td>
<td>{{ record['fields']['Formation'] }}</td>
<td>{{ (profs.get(record['fields']['Professeur'][0]) if 'Professeur' in record['fields'] else {}).get('fields', {}).get('Name')|default('Not Available') }}</td>
<td><a class="button is-small is-primary" href="{{ url_for('airtable.edit_record', record_id=record['id']) }}">Edit</a></td>
</tr>
</{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% block navbar %}
<nav class="tabs is-boxed is-medium is-centered">
<ul>
<li class="{{ 'is-active' if request.path == '/dashboard' else '' }}">
<a href="/dashboard">Tableau de bord</a>
</li>
<li class="{{ 'is-active' if request.path == '/participants' else '' }}">
<a href="/participants">Participants</a>
</li>
<li class="{{ 'is-active' if request.path == '/my_modules' else '' }}">
<a href="/my_modules">Matières</a>
</li>
<li class="{{ 'is-active' if request.path == '/my_mentorat' else '' }}">
<a href="/my_mentorat">Mentorat</a>
</li>
</ul>
</nav>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends 'airtable_base.html' %}
{% block content %}
{{ super() }}
<div class="column is-9">
<section class="section">
<div class="container">
<h2 class="title">Participant: {{ record['fields']['Nom complet'] }}</h2>
<ul>
<li>{{ record['fields']['Nom'] }}</li>
<li>{{ record['fields']['Prénom'] }}</li>
<li>{{ record['fields']['Sexe']}}</li>
<li>{{ record['fields']['Date de naissance'] }}</li>
<li>{{ record['fields']['Adresse complète'] }}</li>
<li>{{ sessions.get(record['fields']['Session Passe Numérique'][0])['fields']['Name']}}</li>
<li>{{ sessions.get(record['fields']['Session du Bac +1'][0])['fields']['Name']}}</li>
</ul>
</div>
</section>
</div>
{% endblock %}

View File

@ -0,0 +1,121 @@
{% extends 'airtable_base.html' %}
{% block content %}
<div class="column is-three-quarters-desktop box is-centered has-text-centered">
<h2 class="title">Edit Participant</h2>
<form method="POST" enctype="multipart/form-data" id="editRecordForm">
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" for="family_name">Nom de famille</label>
</div>
<div class="field-body has-text-left">
<div class="field">
<div class="control">
<input id="family_name" name="family_name" value="{{ record.fields.Nom }}" required><br><br>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" for="first_name">Prénom</label>
</div>
<div class="field-body has-text-left">
<div class="field">
<div class="control">
<input id="first_name" name="first_name" value="{{ record.fields.Prénom }}" required><br><br>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">CNI</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="columns is-multiline" id="CNI-columns">
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
</div>
<div class="card-footer">
<div class="button card-footer-item" id="add-image-label-CNI" data-field="CNI" data-columns-container="CNI-columns">Add</div>
</div>
</div>
</div>
{% for attachment in record.fields.CNI %}
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<a href="{{ attachment.url }}" target="_blank">
<img class="thumbnail" id="thumbnail_CNI_{{ attachment.id }}" src="{{ attachment.thumbnails.large.url }}"></img>
<img id="red_cross_CNI_{{ attachment.id }}" src="{{ url_for('static', filename='svg/full-cross-circle-alt-svgrepo-com.svg')}}" alt="Red Cross" style="display: none;">
</a>
</figure>
</div>
<div class="card-footer">
<div class="button card-footer-item existing-remove-label" id="remove_label_CNI_{{ attachment.id }}" data-field="CNI" data-attachment-id="{{ attachment.id }}">Remove</div>
</div>
<input type="file" name="CNI_{{ attachment.id }}" id="CNI_{{ attachment.id }}" field-data="CNI" value="{{ attachment.url }}" style="display: none;">
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Attestation Pôle Emploi</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="columns is-multiline" id="Attestation-columns">
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
</div>
<div class="card-footer">
<div class="button card-footer-item" id="add-image-label-Attestation" data-field="Attestation" data-columns-container="Attestation-columns">Add</div>
</div>
</div>
</div>
{% for attachment in record.fields['Attestation Pôle Emploi'] %}
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<a href="{{ attachment.url }}" target="_blank">
<img class="thumbnail" id="thumbnail_Attestation_{{ attachment.id }}" src="{{ attachment.thumbnails.large.url }}"></img>
<img id="red_cross_Attestation_{{ attachment.id }}" src="{{ url_for('static', filename='svg/full-cross-circle-alt-svgrepo-com.svg')}}" alt="Red Cross" style="display: none;">
</a>
</figure>
</div>
<div class="card-footer">
<div class="button card-footer-item existing-remove-label" id="remove_label_Attestation_{{ attachment.id }}" data-field="Attestation" class="card-footer-item" data-attachment-id="{{ attachment.id }}">Remove</div>
</div>
<input type="file" name="Attestation_{{ attachment.id }}" id="Attestation_{{ attachment.id }}" field-data="Attestation" value="{{ attachment.url }}" style="display: none;">
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="control center">
<input class="button is-primary" type="submit" value="Submit">
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends 'airtable_base.html' %}
{% block content %}
<div class="column is-9">
<section class="section">
<div class="container">
<h2 class="title">Participants</h2>
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Nom</th>
<th>Date de naissance</th>
<th>Sexe</th>
<th>Adresse</th>
<th>CNI</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr>
<td>{{ record['fields']['Nom complet'] }}</td>
<td>{{ record['fields']['Date de naissance'] }}</td>
<td>{{ record['fields']['Sexe'] }}</td>
<td>{{ record['fields']['Adresse complète'] }}</td>
<td>
{% for attachment in record['fields']['CNI'] %}
<a href="{{ attachment['url']}}" ><img src="{{ attachment['thumbnails']['small']['url']}}"></img></a>
{% endfor %}
</td>
<td>
<a class="button is-small is-primary" href="{{ url_for('airtable.display_participant_form', participant_number=record['fields']['Number']) }}">Voir</a>
<a class="button is-small is-primary" href="{{ url_for('airtable.edit_participant', participant_number=record['fields']['Number']) }}">Edit</a>
</td>
</tr>
</{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "airtable_base.html" %}
{% block content %}
{{ super() }}
<h1>Your form has been submitted successfully!</h1>
<a href="{{ url_for('airtable.create_record') }}">Submit another response</a>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends "security/base.html" %}
{% block metas %}
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
{% endblock %}
{%- block head_scripts %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.css" integrity="sha512-SI0aF82pT58nyOjCNfyeE2Y5/KHId8cLIX/1VYzdjTRs0HPNswsJR+aLQYSWpb88GDJieAgR4g1XWZvUROQv1A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{%- endblock head_scripts %}
{%- block styles %}
<style>
.hide { display: none;}
.fs-center { text-align: center }
.fs-important { font-size: larger; font-weight: bold }
.fs-gap { margin-top: 20px; }
.fs-div { margin: 4px; }
.fs-error-msg { color: darkred; }
</style>
{%- endblock %}
{% block body %}
<section class="hero is-primary is-fullheight">
<div class="hero-head">
{% block navbar %}
{% include "partials/navbar.html" %}
{% endblock navbar %}
</div>
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
{% block content -%}
{%- endblock content %}
</div>
</div>
</div>
<div class="hero-foot">
{% block footer -%}
{%- endblock footer %}
</div>
</section>
{% block scripts %}
{%- endblock scripts %}
{% endblock body %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block content %}
<div class="column is-half">
<h1 class="title">
Flask Login Example
</h1>
<h2 class="subtitle">
Easy authentication and authorization in Flask.
</h2>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% block navbar %}
<nav class="navbar" role="navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item">
<img src="https://bulma.io/images/bulma-type-white.png" alt="Logo">
</a>
<span class="navbar-burger" data-target="navbarMenuHeroA">
<span></span>
<span></span>
<span></span>
</span>
</div>
<div id="navbarMenuHeroA" class="navbar-menu">
<div class="navbar-end">
<a href="{{ url_for('views.home') }}" class="navbar-item">
Home
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('views.profile') }}" class="navbar-item">
Profile
</a>
<a href="{{ url_for('airtable.airtable_dashboard') }}" class="navbar-item">
Airtable
</a>
<a href="{{ url_for('security.logout') }}" class="navbar-item">
Logout
</a>
{% endif %}
{% if not current_user.is_authenticated %}
<a href="{{ url_for('security.login') }}" class="navbar-item">
Login
</a>
{% endif %}
</div>
</div>
</div>
</nav>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="hero-body">
<div class="container has-text-centered">
<h1>Hello {{name}}</h1>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,55 @@
{% macro render_field_with_errors(field) %}
<div class="field" id="{{ field.id|default('fs-field') }}">
<div class="control">
{% set placeholder = field.label.text %}
{{ field(placeholder=placeholder,**kwargs)|safe }}
</div>
{% if field.errors %}
<ul>
{% for error in field.errors %}
<li class="fs-error-msg">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
{% macro render_field(field) %}
<div class="fs-div field" id="{{ field.id|default('fs-field') }}">
{% if field.label.text == "Login" %}
{{ field(**kwargs)|safe }}
{% else %}
{{ field.label }} {{ field(**kwargs)|safe }}
{% endif %}
</div>
{% endmacro %}
{% macro render_field_errors(field) %}
<div class="fs-div" id="{{ field.id if field else 'fs-field' }}">
{% if field and field.errors %}
<ul>
{% for error in field.errors %}
<li class="fs-error-msg">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
{# render WTForms (>3.0) form level errors #}
{% macro render_form_errors(form) %}
{% if form.form_errors %}
<div class="fs-div field" id="fs-form-errors">
<ul>
{% for error in form.form_errors %}
<li class="fs-error-msg">{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endmacro %}
{% macro prop_next() -%}
{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}
{%- endmacro %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block content %}
{% include "security/_messages.html" %}
<h1>{{ _fsdomain('Send password reset instructions') }}</h1>
<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
{{ render_field_with_errors(forgot_password_form.email) }}
{{ render_field(forgot_password_form.submit, class="button") }}
</form>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors, render_form_errors, prop_next %}
{% block content %}
<div class="column is-one-third">
{% include "security/_messages.html" %}
<h1 class="title">{{ _fsdomain('Login') }}</h1>
<div class="block">
<form action="{{ url_for_security('login') }}{{ prop_next() }}" method="POST" name="login_user_form">
<div class="box">
{{ login_user_form.hidden_tag() }}
{{ render_form_errors(login_user_form) }}
{% if "email" in identity_attributes %}
{{ render_field_with_errors(login_user_form.email) }}
{% endif %}
{% if login_user_form.username and "username" in identity_attributes %}
{% if "email" in identity_attributes %}
<h3>{{ _fsdomain("or") }}</h3>
{% endif %}
{{ render_field_with_errors(login_user_form.username) }}
{% endif %}
{{ render_field_with_errors(login_user_form.password) }}
</div>
{{ render_field(login_user_form.remember) }}
{{ render_field_errors(login_user_form.csrf_token) }}
{{ render_field(login_user_form.submit, class="button is-rounded is-primary", label="bonjour") }}
</form>
{% if security.webauthn %}
<hr class="fs-gap">
<h2>{{ _fsdomain("Use WebAuthn to Sign In") }}</h2>
<div>
<form method="GET" id="wan-signin-form" name="wan_signin_form">
<input id="wan_signin" name="wan_signin" type="submit" value="{{ _fsdomain('Sign in with WebAuthn') }}"
formaction="{{ url_for_security('wan_signin') }}{{ prop_next() }}">
</form>
</div>
{% endif %}
{% if security.oauthglue %}
<hr class="fs-gap">
<h2>{{ _fsdomain("Use Social Oauth to Sign In") }}</h2>
{% for provider in security.oauthglue.provider_names %}
<div class="fs-gap">
<form method="POST" id={{ provider }}-form name={{ provider }}_form>
<input id={{ provider }} name={{ provider }} type="submit" value="{{ _fsdomain('Sign in with ')~provider }}"
formaction="{{ url_for_security('oauthstart', name=provider) }}{{ prop_next() }}">
</form>
</div>
{% endfor %}
{% endif %}
{% if google_login_url is defined %}
<a class="button is-rounded is-primary" href="{{ google_login_url() }}" class="button is-primary">
<span class="icon">
<i class="fab fa-google"></i>
</span>
<span>Sign in with Google</span>
</a>
{% endif %}
{% if security.recoverable %}
<a class="button is-rounded is-primary" href="{{ url_for_security('forgot_password') }}">{{ _fsdomain('Forgot password') }}</a><br/>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "security/base.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field, render_form_errors %}
{% block content %}
{% include "security/_messages.html" %}
<h1>{{ _fsdomain('Register') }}</h1>
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
{{ register_user_form.hidden_tag() }}
{{ render_form_errors(register_user_form) }}
{{ render_field_with_errors(register_user_form.email) }}
{% if security.username_enable %}
{{ render_field_with_errors(register_user_form.username) }}
{% endif %}
{{ render_field_with_errors(register_user_form.password) }}
{% if register_user_form.password_confirm %}
{{ render_field_with_errors(register_user_form.password_confirm) }}
{% endif %}
{{ render_field(register_user_form.submit) }}
</form>
{% include "security/_menu.html" %}
{% endblock %}

View File

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block content %}
<h2>User Management</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-primary btn-sm">Edit</a>
{% if not user.is_admin %}
{% if user.is_active %}
<a href="{{ url_for('block_user', user_id=user.id) }}" class="btn btn-warning btn-sm">Block</a>
{% else %}
<a href="{{ url_for('unblock_user', user_id=user.id) }}" class="btn btn-success btn-sm">Unblock</a>
{% endif %}
<a href="{{ url_for('delete_user', user_id=user.id) }}" class="btn btn-danger btn-sm">Delete</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row">
<div class="col-md-6">
<h3>Create User</h3>
<form method="POST" action="{{ url_for('create_user') }}">
{{ form.csrf_token }}
<div class="form-group">
<label for="username">Username</label>
{{ form.username(class="form-control", placeholder="Enter username") }}
</div>
<div class="form-group">
<label for="email">Email</label>
{{ form.email(class="form-control", placeholder="Enter email address") }}
</div>
<div class="form-group">
<label for="password">Password</label>
{{ form.password(class="form-control", placeholder="Enter password") }}
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
<div class="col-md-6">
<h3>Change Password</h3>
<form method="POST" action="{{ url_for('change_password') }}">
{{ form.csrf_token }}
<div class="form-group">
<label for="email">Email</label>
{{ form.email(class="form-control", placeholder="Enter email address") }}
</div>
<div class="form-group">
<label for="password">New Password</label>
{{ form.password(class="form-control", placeholder="Enter new password") }}
</div>
<button type="submit" class="btn btn-primary">Change</button>
</form>
</div>
</div>
{% endblock %}

37
project/views.py Normal file
View File

@ -0,0 +1,37 @@
# Views
from flask import Blueprint, render_template, redirect, url_for
from flask_security import current_user, auth_required
from .models import User, Role
from .database import db_session
views_bp = Blueprint('views', __name__)
@views_bp.route('/')
def home():
return render_template('index.html')
@views_bp.route('/profile')
@auth_required()
def profile():
if not current_user.is_authenticated:
return redirect(url_for('security.login'))
else:
return render_template('profile.html', name=current_user.email)
''' @app.route('/users', endpoint='users', methods=['GET', 'POST'])
@auth_required
def users():
form = CreateUserForm()
if form.validate_on_submit():
user = User(email=form.email.data, password=form.password.data)
db.session.add(user)
db.session.commit()
flash('User created successfully!', 'success')
return redirect(url_for('users'))
users = User.query.all()
return render_template('users.html', users=users, form=form) '''

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
Flask-Security-Too[common]==5.1.0
SQLAlchemy==2.0.4
python-dotenv
Flask-Admin==1.6.1
Flask-WTF==1.1.1
Font-Awesome-Flask==0.1.1
airtable-python-wrapper==0.15.3
Flask-Dance==6.2.0
google-auth==2.17.0
google-auth-httplib2==0.1.0
google-auth-oauthlib==1.0.0
gunicorn==20.1.0
oauth2client==4.1.3