first commit

This commit is contained in:
Éric Li 2021-05-06 10:26:28 +02:00
commit 361bae10ac
19 changed files with 607 additions and 0 deletions

BIN
app.db Normal file

Binary file not shown.

11
app/__init__.py Normal file
View File

@ -0,0 +1,11 @@
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
from app import routes, models

97
app/forms.py Normal file
View File

@ -0,0 +1,97 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed
from wtforms import StringField, SubmitField, FileField, TextAreaField, RadioField
from wtforms.validators import DataRequired, NumberRange, Length, Regexp
import yaml
import os
REPERTOIRE_YAML = "./export/"
if not os.path.exists(REPERTOIRE_YAML):
os.makedirs(REPERTOIRE_YAML)
class Form(FlaskForm):
exporter = SubmitField("Exporter")
fichier = FileField("Choisir fichier", validators=[FileAllowed(["yml"], "Fichier Yaml seulement!")])
importer = SubmitField("Importer")
class PNForm(Form):
file_length = len("PN0")
code = StringField("Code", validators=[DataRequired(), Length(3,3)])
nom = StringField("Nom", validators=[DataRequired()] )
diminutif = StringField("Diminutif", validators=[DataRequired()] )
description = TextAreaField("Description", validators=[DataRequired()] )
type = RadioField("Type", choices=[1,2,3], validators=[DataRequired()])
class ACForm(Form):
file_length = len("AC0000.yml")
code = StringField("Code", validators=[DataRequired(), Length(6,6)])
class SAEForm(Form):
file_length = len("SAE00.yml")
code = StringField("Code", validators=[DataRequired(), Length(5,5)])
titre = StringField("Titre", validators=[DataRequired()] )
semestre = StringField("Semestre", validators=[DataRequired()] )
heures_encadrees = StringField("Heures encadrées", validators=[DataRequired()] )
heures_tp = StringField("Heures TP", validators=[DataRequired()] )
projet = StringField("Projet", validators=[DataRequired()] )
description = TextAreaField("Description", validators=[DataRequired()] )
coef = StringField("Coef.", validators=[DataRequired()] )
acs = StringField("ACs", validators=[DataRequired()] )
ressources = StringField("Ressources", validators=[DataRequired()] )
livrables = TextAreaField("Livrables", validators=[DataRequired()] )
motscles = StringField("Mots clés", validators=[DataRequired()] )
class RessourceForm(Form):
file_length = len("R000.yml")
code = StringField("Code", validators=[DataRequired(), Length(4,4)])
nom = StringField("Nom", validators=[DataRequired()] )
semestre = StringField("Semestre", validators=[DataRequired()] )
heures_formation = StringField("Heures formation", validators=[DataRequired()] )
heures_tp = StringField("Heures TP", validators=[DataRequired()] )
coef = StringField("Coef.", validators=[DataRequired()] )
acs = StringField("ACs", validators=[DataRequired()] )
saes = StringField("SAEs", validators=[DataRequired()] )
prerequis = StringField("Prérequis", validators=[DataRequired()] )
contexte = TextAreaField("Contexte", validators=[DataRequired()] )
contenu = TextAreaField("Contenu", validators=[DataRequired()] )
motscles = StringField("Mots clés", validators=[DataRequired()] )
class CompetenceForm(Form):
file_length = len("RT0.yml")
code = StringField("Code", validators=[DataRequired(), Length(3,3)])
nom = StringField("Nom", validators=[DataRequired()] )
diminutif = StringField("Diminutif", validators=[DataRequired()] )
description = TextAreaField("Description", validators=[DataRequired()] )
composantes = TextAreaField("Composantes", validators=[DataRequired()] )
situations = TextAreaField("Situations", validators=[DataRequired()] )
niveaux = TextAreaField("Niveaux", validators=[DataRequired()] )
def form_import(form):
""" Si import a été appuyé et qu'il n'y a pas d'erreur d'import => importe le fichier yaml"""
if form.importer.data and len(form.fichier.errors) == 0 and len(form.fichier.data.filename) == form.file_length:
fichier_Yaml = yaml.safe_load(form.fichier.data.read())
for categorie, valeur in fichier_Yaml.items():
form[categorie].data = valeur
form.validate_on_submit() # Réinitialise les messages d'erreur
return form
def form_export(form):
""" Si le formulaire est valide => exporte dans un fichier yaml avec les informations du formulaire """
output = {}
for categorie, valeur in list(form.data.items())[3:-1]:
output[categorie] = valeur
fichier = REPERTOIRE_YAML + form.code.data + ".yml"
with open(fichier, "w", encoding="utf8") as fid:
fid.write(yaml.dump(output))

11
app/models.py Normal file
View File

@ -0,0 +1,11 @@
from app import db
class PN(db.Model):
code = db.Column(db.String(3), primary_key = True)
nom = db.Column(db.String(255))
diminutif = db.Column(db.String(30))
description = db.Column(db.Text())
type = db.Column(db.Integer())
def __repr__(self):
return "<PN {}>".format(self.code)

59
app/routes.py Normal file
View File

@ -0,0 +1,59 @@
from flask import render_template, flash, redirect, url_for, request
from app import app
from app.forms import *
from app.models import PN
import yaml
@app.route("/")
@app.route("/index")
def index():
return render_template("base.html")
@app.route("/PN", methods=["GET","POST"])
def PN():
form = PNForm()
form_validation = form.validate_on_submit()
form = form_import(form)
if form_validation and form.exporter.data:
flash("Ajout du référentiel PN: {} ".format(form.code.data))
form_export(form)
return redirect(url_for("PN"))
return render_template("PN.html", form = form)
@app.route("/AC", methods=["GET","POST"])
def AC():
return render_template("PN.html", form = form)
@app.route("/SAE", methods=["GET","POST"])
def SAE():
form = SAEForm()
form_validation = form.validate_on_submit()
form = form_import(form)
if form_validation and form.exporter.data:
flash("Ajout du référentiel SAE: {} ".format(form.code.data))
form_export(form)
return redirect(url_for("SAE"))
return render_template("SAE.html", form = form)
@app.route("/Ressource", methods=["GET","POST"])
def Ressource():
form = RessourceForm()
form_validation = form.validate_on_submit()
form = form_import(form)
if form_validation and form.exporter.data:
flash("Ajout du référentiel Ressource: {} ".format(form.code.data))
form_export(form)
return redirect(url_for("Ressource"))
return render_template("Ressource.html", form = form)
@app.route("/Competence", methods=["GET","POST"])
def Competence():
form = CompetenceForm()
form_validation = form.validate_on_submit()
form = form_import(form)
if form_validation and form.exporter.data:
flash("Ajout du référentielCompetence: {} ".format(form.code.data))
form_export(form)
return redirect(url_for("Competence"))
return render_template("Competence.html", form = form)

View File

@ -0,0 +1,16 @@
{% extends "form.html" %}
{% block title %}RT Form{% endblock %}
{% block form_title %}Formulaire de Compétences (RT){% endblock %}
{% block formulaire %}
{{ render_field(form.code,"input") }}
{{ render_field(form.nom,"input") }}
{{ render_field(form.diminutif,"input") }}
{{ render_field(form.description,"textarea") }}
{{ render_field(form.composantes,"textarea") }}
{{ render_field(form.situations,"textarea") }}
{{ render_field(form.niveaux,"textarea") }}
{% endblock %}

28
app/templates/PN.html Normal file
View File

@ -0,0 +1,28 @@
{% extends "form.html" %}
{% block title %}PN Form{% endblock %}
{% block form_title %}Formulaire de Programmes Nationaux (PN){% endblock %}
{% block formulaire %}
{{ render_field(form.code,"input") }}
{{ render_field(form.nom,"input") }}
{{ render_field(form.diminutif,"input") }}
{{ render_field(form.description,"textarea") }}
<div class="field">
<label class="label">{{ form.type.label }}</label>
<div class="control">
{% for num in form.type %}
<label class="radio">
{{ num(class="radio") }}
{{ num.label }}
</label>
{% endfor %}
</div>
{% for error in form.type.errors %}
<p class="help is-danger">{{error}}</p>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "form.html" %}
{% block title %}Ressource Form{% endblock %}
{% block form_title %}Formulaire de Ressources (R){% endblock %}
{% block formulaire %}
{{ render_field(form.code,"input") }}
{{ render_field(form.nom,"input") }}
{{ render_field(form.semestre,"input") }}
{{ render_field(form.heures_formation,"input") }}
{{ render_field(form.heures_tp,"input") }}
{{ render_field(form.coef,"input") }}
{{ render_field(form.acs,"input") }}
{{ render_field(form.saes,"input") }}
{{ render_field(form.prerequis,"input") }}
{{ render_field(form.contexte,"textarea") }}
{{ render_field(form.contenu,"textarea") }}
{{ render_field(form.motscles,"input") }}
{% endblock %}

21
app/templates/SAE.html Normal file
View File

@ -0,0 +1,21 @@
{% extends "form.html" %}
{% block title %}SAE Form{% endblock %}
{% block form_title %}Formulaire de Situations d'apprentissages et d'évaluations (SAE){% endblock %}
{% block formulaire %}
{{ render_field(form.code,"input") }}
{{ render_field(form.titre,"input") }}
{{ render_field(form.semestre,"input") }}
{{ render_field(form.heures_encadrees,"input") }}
{{ render_field(form.heures_tp,"input") }}
{{ render_field(form.projet,"input") }}
{{ render_field(form.description,"textarea") }}
{{ render_field(form.coef,"input") }}
{{ render_field(form.acs,"input") }}
{{ render_field(form.ressources,"input") }}
{{ render_field(form.livrables,"textarea") }}
{{ render_field(form.motscles,"input") }}
{% endblock %}

54
app/templates/base.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html class="has-navbar-fixed-top">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.2/css/bulma.min.css">
<link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
{% block head %}{% endblock %}
</head>
<body>
<!-- Barre de navigation -->
<nav class="navbar is-primary is-spaced is-fixed-top py-0">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item is-size-5" href="https://scodoc.org/">ScoDoc</a>
<!-- Menu deroulant affiché uniquement sur mobile -->
<div class="navbar-burger">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<!-- Liste des catégories -->
<a class="navbar-item" href="{{ url_for('PN') }}">PN</a>
<a class="navbar-item" href="{{ url_for('AC') }}">AC</a>
<a class="navbar-item" href="{{ url_for('SAE') }}">SAÉ</a>
<a class="navbar-item" href="{{ url_for('Ressource') }}">Ressource</a>
<a class="navbar-item" href="{{ url_for('Competence') }}">Competence</a>
</div>
<div class="navbar-end">
</div>
</div>
</div>
</nav>
<!-- Contenu de la page -->
<div class="container">
<div class="box">
{% with messages = get_flashed_messages() %}
{% if messages %}
<script>
alert("{% for message in messages %}{{message}}{% endfor %}");
</script>
{% endif %}
{% endwith %}
{% block content %}
{% endblock %}
</div>
</div>
</body>
</html>

48
app/templates/form.html Normal file
View File

@ -0,0 +1,48 @@
{% macro render_field(field,class) %}
<div class="field">
<label class="label">{{ field.label }}</label>
<div class="control">
{{ field(class=class)}}
</div>
{% for error in field.errors %}
<p class="help is-danger">{{error}}</p>
{% endfor %}
</div>
{% endmacro %}
{% extends "base.html" %}
{% block content %}
<form action="" enctype=multipart/form-data method="post" novalidate>
{{ form.hidden_tag() }}
<div class="content">
<h1 class="title">{% block form_title %}{% endblock %}</h1>
{% block formulaire %}{% endblock %}
<div class="field is-grouped">
<div class="control">
{{ form.exporter(class="button")}}
</div>
<div class="control">
<div class="file has-name">
<label class="file-label">
{{ form.fichier(class="file-input") }}
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-file-import"></i>
</span>
<span class="file-label">
{{ form.fichier.label}}
</span>
</span>
{{ form.importer(class="button file-name", accept=".yml")}}
</label>
</div>
{% for error in form.fichier.errors %}
<p class="help is-danger">{{error}}</p>
{% endfor %}
</div>
</div>
</div>
</form>
{% endblock %}

7
config.py Normal file
View File

@ -0,0 +1,7 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
SECRET_KEY = os.environ.get("SECRET_KEY") or 'unStringRandomDuneLongueurRandom'
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or "sqlite:///" + os.path.join(basedir, "app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False

7
main.py Normal file
View File

@ -0,0 +1,7 @@
from app import app, db
from app.models import PN
@app.shell_context_processor
def make_shell_context():
return {"db": db, "PN": PN}

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

50
migrations/alembic.ini Normal file
View File

@ -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

90
migrations/env.py Normal file
View File

@ -0,0 +1,90 @@
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.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.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()

24
migrations/script.py.mako Normal file
View File

@ -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"}

View File

@ -0,0 +1,34 @@
"""PN table
Revision ID: be67c6934c05
Revises:
Create Date: 2021-05-03 17:01:39.845539
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'be67c6934c05'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('PN',
sa.Column('code', sa.String(length=3), nullable=False),
sa.Column('nom', sa.String(length=255), nullable=True),
sa.Column('diminutif', sa.String(length=30), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('code')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('PN')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""PN table
Revision ID: c1377d41bf27
Revises: be67c6934c05
Create Date: 2021-05-03 17:20:44.055359
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c1377d41bf27'
down_revision = 'be67c6934c05'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('PN', sa.Column('type', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('PN', 'type')
# ### end Alembic commands ###