La gestion des modules est une composante essentielle d'une plateforme de test d'API, permettant d'organiser les cas de test par catégories fonctionnelles. Cette section détaille l'implémentation des opérations CRUD pour les modules, ainsi que leur intégration avec la gestion des projets.
Modèle de Données
Le modèle ApiModule est défini dans models.py. Il établit une relation de clé étrangère avec la tible des projets pour garantir l'intégrité référentielle.
class ApiModule(db.Model, EntityBase):
__tablename__ = 'api_modules'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(200), nullable=False)
category = db.Column(db.String(50), nullable=False)
description = db.Column(db.String(255))
assigned_tester = db.Column(db.String(100))
project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __init__(self, name, category, description, assigned_tester, project_id):
self.name = name
self.category = category
self.description = description
self.assigned_tester = assigned_tester
self.project_id = project_id
Après la mise à jour du modèle, les migrations sont appliquées via Flask-Migrate :
flask db migrate -m "Add api_modules table"
flask db upgrade
Logique Backend (Flask)
Création et Modification
Les points de terminaison pour l'ajout et la mise à jour utilisent des requêtes JSON pour faciliter l'intégration avec le frontend.
@module_bp.route('/create', methods=['POST'])
def create_module():
payload = request.get_json()
try:
new_module = ApiModule(
name=payload.get('name'),
category=payload.get('category'),
description=payload.get('description'),
assigned_tester=payload.get('assigned_tester'),
project_id=payload.get('project_id')
)
db.session.add(new_module)
db.session.commit()
return jsonify({"status": "success", "message": "Module créé avec succès."}), 201
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)}), 500
@module_bp.route('/update/<int:module_id>', methods=['PUT'])
def update_module(module_id):
payload = request.get_json()
module = ApiModule.query.get_or_404(module_id)
try:
module.name = payload.get('name', module.name)
module.category = payload.get('category', module.category)
module.description = payload.get('description', module.description)
module.assigned_tester = payload.get('assigned_tester', module.assigned_tester)
module.project_id = payload.get('project_id', module.project_id)
db.session.commit()
return jsonify({"status": "success", "message": "Module mis à jour."})
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)}), 500
Suppression et Contraintes
La suprpession prend en charge les opérations en lot. De plus, la suppression d'un projet est bloquée si des modules y sont associés.
@module_bp.route('/delete', methods=['DELETE'])
def delete_modules():
payload = request.get_json()
module_ids = payload.get('ids', [])
if not module_ids:
return jsonify({"status": "error", "message": "Aucun ID fourni."}), 400
try:
ApiModule.query.filter(ApiModule.id.in_(module_ids)).delete(synchronize_session=False)
db.session.commit()
return jsonify({"status": "success", "message": "Modules supprimés."})
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)}), 500
@project_bp.route('/delete', methods=['DELETE'])
def delete_projects():
payload = request.get_json()
project_ids = payload.get('ids', [])
linked_modules = ApiModule.query.filter(ApiModule.project_id.in_(project_ids)).count()
if linked_modules > 0:
return jsonify({"status": "error", "message": "Impossible de supprimer : des modules sont liés à ces projets."}), 400
try:
Project.query.filter(Project.id.in_(project_ids)).delete(synchronize_session=False)
db.session.commit()
return jsonify({"status": "success", "message": "Projets supprimés."})
except Exception as e:
db.session.rollback()
return jsonify({"status": "error", "message": str(e)}), 500
Liste et Options
L'API de liste renvoie les données paginées, tandis que l'API des options fournit les projets pour les menus déroulants sous forme de JSON.
@module_bp.route('/list', methods=['GET'])
def list_modules():
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 10, type=int)
project_id = request.args.get('project_id', type=int)
query = db.session.query(ApiModule, Project).join(Project, ApiModule.project_id == Project.id)
if project_id:
query = query.filter(ApiModule.project_id == project_id)
query = query.order_by(ApiModule.created_at.desc())
pagination = query.paginate(page=page, per_page=limit, error_out=False)
data = [{
"id": m.ApiModule.id,
"name": m.ApiModule.name,
"category": m.ApiModule.category,
"description": m.ApiModule.description,
"assigned_tester": m.ApiModule.assigned_tester,
"created_at": m.ApiModule.created_at.strftime('%Y-%m-%d %H:%M:%S'),
"project_id": m.Project.id,
"project_name": m.Project.name
} for m in pagination.items]
return jsonify({"status": "success", "total": pagination.total, "data": data})
@project_bp.route('/options', methods=['GET'])
def get_project_options():
projects = Project.query.with_entities(Project.id, Project.name).all()
options = [{"id": p.id, "name": p.name} for p in projects]
return jsonify({"status": "success", "options": options})
Interface Utilisateur (Layui)
Le fichier modules.html utilise le framework Layui pour afficher le tableau de données et gérer les interactions. Le code JavaScript a été restructuré pour consommer les API JSON et générer dynamiquement les options des formulaires.
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Gestion des Modules</title>
<link href="../static/css/layui.css" rel="stylesheet">
<script src="../static/layui.js"></script>
</head>
<body>
<ul class="layui-nav">
<li class="layui-nav-item"><a href="/">Projets</a></li>
<li class="layui-nav-item layui-this"><a href="/modules">Modules</a></li>
</ul>
<div class="layui-body" style="padding: 20px;">
<table class="layui-hide" id="module-table" lay-filter="module-table"></table>
<script type="text/html" id="table-toolbar">
<div class="layui-btn-container">
<button class="layui-btn" lay-event="add">Ajouter</button>
<button class="layui-btn layui-btn-warm" lay-event="edit">Modifier</button>
<button class="layui-btn layui-btn-danger" lay-event="del">Supprimer</button>
</div>
</script>
</div>
<script>
layui.use(['table', 'layer', 'form'], function(){
var table = layui.table;
var layer = layui.layer;
var form = layui.form;
var $ = layui.$;
function loadProjectOptions(selectedId, callback) {
$.get('/api/projects/options', function(res){
if(res.status === 'success') {
var html = '<option value="">Sélectionner un projet</option>';
res.options.forEach(function(opt){
var isSelected = (opt.id == selectedId) ? 'selected' : '';
html += '<option value="' + opt.id + '" ' + isSelected + '>' + opt.name + '</option>';
});
callback(html);
}
});
}
table.render({
elem: '#module-table',
url: '/api/modules/list',
method: 'get',
page: true,
limit: 10,
cols: [[
{type: 'checkbox', fixed: 'left'},
{field: 'id', title: 'ID', width: 80},
{field: 'name', title: 'Nom du Module'},
{field: 'project_name', title: 'Projet Associé'},
{field: 'category', title: 'Catégorie'},
{field: 'assigned_tester', title: 'Testeur'},
{field: 'created_at', title: 'Date de Création', width: 160}
]],
toolbar: '#table-toolbar',
defaultToolbar: ['filter', 'exports']
});
table.on('toolbar(module-table)', function(obj){
var checkStatus = table.checkStatus(obj.config.id);
var data = checkStatus.data;
if(obj.event === 'add'){
layer.open({
type: 1,
title: 'Ajouter un Module',
area: ['450px', '400px'],
content: $('#form-template').html(),
success: function(layero){
loadProjectOptions('', function(html){
layero.find('#project-select').html(html);
form.render('select');
});
}
});
} else if(obj.event === 'del'){
if(data.length === 0) return layer.alert('Veuillez sélectionner des éléments.');
var ids = data.map(function(item){ return item.id; });
layer.confirm('Confirmer la suppression ?', function(index){
$.ajax({
url: '/api/modules/delete',
type: 'DELETE',
contentType: 'application/json',
data: JSON.stringify({ids: ids}),
success: function(res){
layer.close(index);
layer.msg(res.message);
table.reload('module-table');
}
});
});
}
});
});
</script>
<script type="text/html" id="form-template">
<form class="layui-form" style="padding: 20px;">
<div class="layui-form-item">
<label class="layui-form-label">Nom</label>
<div class="layui-input-block">
<input type="text" name="name" required lay-verify="required" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Projet</label>
<div class="layui-input-block">
<select name="project_id" id="project-select" lay-verify="required"></select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Catégorie</label>
<div class="layui-input-block">
<select name="category" lay-verify="required">
<option value="app">Application</option>
<option value="web">Web</option>
<option value="backend">Backend</option>
</select>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn" lay-submit lay-filter="submit-module">Valider</button>
</div>
</div>
</form>
</script>
</body>
</html>
Pour finir, ajoutez le lien de navigation dans le menu principal de project.html :
<li class="layui-nav-item"><a href="/modules">Gestion des Modules</a></li>