Initial commit.

This commit is contained in:
Jeff Lance 2019-03-25 09:19:04 +01:00
parent 2eca8499dc
commit 58245c1345
37 changed files with 2534 additions and 0 deletions

17
Pipfile Normal file
View File

@ -0,0 +1,17 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
colorclass = "==2.2.0"
numpy = "==1.14.3"
pandas = "==0.22.0"
six = "==1.11.0"
terminaltables = "==3.1.0"
request = "*"
[dev-packages]
[requires]
python_version = "3.6"

138
Pipfile.lock generated Normal file
View File

@ -0,0 +1,138 @@
{
"_meta": {
"hash": {
"sha256": "4ab96f17ddd44492eb549ca013af03d1ea82744d4cbfb5dda01a1a47b2a7c3ed"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"colorclass": {
"hashes": [
"sha256:b05c2a348dfc1aff2d502527d78a5b7b7e2f85da94a96c5081210d8e9ee8e18b"
],
"index": "pypi",
"version": "==2.2.0"
},
"get": {
"hashes": [
"sha256:9177b24df2b5be1ba86c6d6c2afbee8a77b716ab1b5bb11fcbdf65c3f1decf1a"
],
"version": "==1.0.3"
},
"numpy": {
"hashes": [
"sha256:0074d42e2cc333800bd09996223d40ec52e3b1ec0a5cab05dacc09b662c4c1ae",
"sha256:034717bfef517858abc79324820a702dc6cd063effb9baab86533e8a78670689",
"sha256:0db6301324d0568089663ef2701ad90ebac0e975742c97460e89366692bd0563",
"sha256:1864d005b2eb7598063e35c320787d87730d864f40d6410f768fe4ea20672016",
"sha256:46ce8323ca9384814c7645298b8b627b7d04ce97d6948ef02da357b2389d6972",
"sha256:510863d606c932b41d2209e4de6157ab3fdf52001d3e4ad351103176d33c4b8b",
"sha256:560e23a12e7599be8e8b67621396c5bc687fd54b48b890adbc71bc5a67333f86",
"sha256:57dc6c22d59054542600fce6fae2d1189b9c50bafc1aab32e55f7efcc84a6c46",
"sha256:760550fdf9d8ec7da9c4402a4afe6e25c0f184ae132011676298a6b636660b45",
"sha256:8670067685051b49d1f2f66e396488064299fefca199c7c80b6ba0c639fedc98",
"sha256:9016692c7d390f9d378fc88b7a799dc9caa7eb938163dda5276d3f3d6f75debf",
"sha256:98ff275f1b5907490d26b30b6ff111ecf2de0254f0ab08833d8fe61aa2068a00",
"sha256:9ccf4d5c9139b1e985db915039baa0610a7e4a45090580065f8d8cb801b7422f",
"sha256:a8dbab311d4259de5eeaa5b4e83f5f8545e4808f9144e84c0f424a6ee55a7b98",
"sha256:aaef1bea636b6e552bbc5dae0ada87d4f6046359daaa97a05a013b0169620f27",
"sha256:b8987e30d9a0eb6635df9705a75cf8c4a2835590244baecf210163343bc65176",
"sha256:c3fe23df6fe0898e788581753da453f877350058c5982e85a8972feeecb15309",
"sha256:c5eb7254cfc4bd7a4330ad7e1f65b98343836865338c57b0e25c661e41d5cfd9",
"sha256:c80fcf9b38c7f4df666150069b04abbd2fe42ae640703a6e1f128cda83b552b7",
"sha256:e33baf50f2f6b7153ddb973601a11df852697fba4c08b34a5e0f39f66f8120e1",
"sha256:e8578a62a8eaf552b95d62f630bb5dd071243ba1302bbff3e55ac48588508736",
"sha256:f22b3206f1c561dd9110b93d144c6aaa4a9a354e3b07ad36030df3ea92c5bb5b",
"sha256:f39afab5769b3aaa786634b94b4a23ef3c150bdda044e8a32a3fc16ddafe803b"
],
"index": "pypi",
"version": "==1.14.3"
},
"pandas": {
"hashes": [
"sha256:02541a4fdd31315f213a5c8e18708abad719ee03eda05f603c4fe973e9b9d770",
"sha256:052a66f58783a59ea38fdfee25de083b107baa81fdbe38fabd169d0f9efce2bf",
"sha256:06efae5c00b9f4c6e6d3fe1eb52e590ff0ea8e5cb58032c724e04d31c540de53",
"sha256:12f2a19d0b0adf31170d98d0e8bcbc59add0965a9b0c65d39e0665400491c0c5",
"sha256:244ae0b9e998cfa88452a49b20e29bf582cc7c0e69093876d505aec4f8e1c7fe",
"sha256:2907f3fe91ca2119ac3c38de6891bbbc83333bfe0d98309768fee28de563ee7a",
"sha256:44a94091dd71f05922eec661638ec1a35f26d573c119aa2fad964f10a2880e6c",
"sha256:587a9816cc663c958fcff7907c553b73fe196604f990bc98e1b71ebf07e45b44",
"sha256:66403162c8b45325a995493bdd78ad4d8be085e527d721dbfa773d56fbba9c88",
"sha256:68ac484e857dcbbd07ea7c6f516cc67f7f143f5313d9bc661470e7f473528882",
"sha256:68b121d13177f5128a4c118bb4f73ba40df28292c038389961aa55ea5a996427",
"sha256:97c8223d42d43d86ca359a57b4702ca0529c6553e83d736e93a5699951f0f8db",
"sha256:af0dbac881f6f87acd325415adea0ce8cccf28f5d4ad7a54b6a1e176e2f7bf70",
"sha256:c2cd884794924687edbaad40d18ac984054d247bb877890932c4d41e3c3aba31",
"sha256:c372db80a5bcb143c9cb254d50f902772c3b093a4f965275197ec2d2184b1e61"
],
"index": "pypi",
"version": "==0.22.0"
},
"post": {
"hashes": [
"sha256:13db715defcc1ab9e987c3a397cfb4e4eaf205023830876fb0979f905f56fd5f"
],
"version": "==1.0.2"
},
"public": {
"hashes": [
"sha256:b923b41a57ef886f4d0aee932aac848506680f3d746326149079f73a6403485f"
],
"version": "==1.0.3"
},
"python-dateutil": {
"hashes": [
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
],
"version": "==2.7.3"
},
"pytz": {
"hashes": [
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
],
"version": "==2018.4"
},
"query-string": {
"hashes": [
"sha256:9d06d013034e9c855e26fac2e67d154d0b556ce0353c2eba7330f2ca79fa1a95"
],
"version": "==1.0.2"
},
"request": {
"hashes": [
"sha256:009397eba1bd73ddc8fdc0745f15ecacdf38d0be2186ef431bc8357a60f0900b"
],
"index": "pypi",
"version": "==1.0.2"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"index": "pypi",
"version": "==1.11.0"
},
"terminaltables": {
"hashes": [
"sha256:f3eb0eb92e3833972ac36796293ca0906e998dc3be91fbe1f8615b331b853b81"
],
"index": "pypi",
"version": "==3.1.0"
}
},
"develop": {}
}

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = sage
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

6
docs/_static/figure1 vendored Normal file
View File

@ -0,0 +1,6 @@
+-------------+-----------+-----------+-----------+-----------+-----------+
| 0 | 1 | 2 | 3 | 4 | 5 |
+-------------+-----------+-----------+-----------+-----------+-----------+
| Muet ou très peu bavard | | | Très bavard |
+-------------+-----------+-----------+-----------+-----------+-----------+

BIN
docs/_static/figure1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

3
docs/_static/figure2 vendored Normal file
View File

@ -0,0 +1,3 @@
+----+-----+---------------------+
| ID | NOM | NIVEAU DE BAVARDAGE |
+----+-----+---------------------+

BIN
docs/_static/figure2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

9
docs/api.rst Normal file
View File

@ -0,0 +1,9 @@
***
API
***
.. toctree::
:maxdepth: 1
:glob:
api/*

8
docs/api/engine.rst Normal file
View File

@ -0,0 +1,8 @@
.. _engine:
========
Engine
========
.. autoclass:: sage.Engine
:members:

8
docs/api/interface.rst Normal file
View File

@ -0,0 +1,8 @@
.. _interface:
=========
Interface
=========
.. automodule:: sage.interface
:members:

8
docs/api/seatingplan.rst Normal file
View File

@ -0,0 +1,8 @@
.. _seatingplan:
===========
SeatingPlan
===========
.. autoclass:: sage.SeatingPlan
:members:

8
docs/api/student.rst Normal file
View File

@ -0,0 +1,8 @@
.. _student:
========
Student
========
.. autoclass:: sage.Student
:members:

194
docs/conf.py Normal file
View File

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
# sys.path.insert(0, os.path.abspath('../spoticla/'))
# -- Project information -----------------------------------------------------
project = 'SAGE (Seating plAn GEnerator)'
copyright = '2018, Jeff LANCE'
author = 'Jeff LANCE'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '0.5'
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinxcontrib.napoleon',
'sphinx.ext.doctest',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.imgmath',
'sphinx.ext.ifconfig',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'fr'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'spoticladoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'spoticla.tex', 'spoticla Documentation',
'Jeff LANCE', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'spoticla', 'spoticla Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'spoticla', 'spoticla Documentation',
author, 'spoticla', 'One line description of project.',
'Miscellaneous'),
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
# -- Extension configuration -------------------------------------------------
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

3
docs/getting-started.rst Normal file
View File

@ -0,0 +1,3 @@
**************
Pour commencer
**************

11
docs/index.rst Normal file
View File

@ -0,0 +1,11 @@
#####################
SAGE's documentation!
#####################
.. toctree::
:maxdepth: 1
intro
getting-started
api
references

483
docs/intro.rst Normal file
View File

@ -0,0 +1,483 @@
########
À propos
########
*************
Problématique
*************
*Contexte: Élèves d'une classe, places assises dans une salle*
*Contraintes:
Le niveau de bavardage d'un élève, les liens sociaux entre élèves*
Comme tout enseignant j'ai été et je suis encore confronté au problème de la
gestion de classe et plus précisément aux interactions efficaces ou inefficaces
des élèves au sein d'une classe.
En effet, les associations d´élèves bavards entre eux peuvent mettre à mal une séance.
Dissocier une binôme productif peut l'être également.
Pour "régler" cela, la démarche consiste à créer un plan de classe afin de diriger
ou limiter les liens que peuvent avoir les élèves entre eux durant une séance.
Ainsi, j'ai tenté plusieurs fois de casser les bavardages entre plusieurs
camarades en les disposant dans la classe à divers endroits et ce dans le
but d'arriver à un équilibre entre bonne dynamique du groupe classe du point
de vue de l'ambiance et du travail et la limitation des bavardages, de l'inattention.
Voulant également passer la certification ISN, il me fallait trouver un projet
qui m'intéressait et qui semblait pertinent pour le dossier à présenter.
*****************
Solution proposée
*****************
*La solution proposée doit:
- utiliser Python
- être simple d'utilisation (IHM, import de la liste des élèves,...)
- proposer un placement automatique des élèves en fonction de certaines contraintes
- permettre d'éditer cette solution de positionnement ou d'en créer une
- permettre de modifier les contraintes*
**********
Conception
**********
1ère phase de recherche
=======================
J'ai tout d'abord commencé par faire des recherches sur cette thématique.
Au hasard de mes recherches, je suis tombé sur une application web, `xperprof`_,
permettant de génerer un plan de classe.
Cette application propose une fonction de positionnement aléatoire des élèves et
notamment la capacité de ne pas soumettre certaines tables à l'algorithme et ainsi
créer des zones vide autour de certains élèves.
Une première idée d'algorithme m'est venue : le positionnement aléatoire.
J'ai toutefois continué mes recherches et ai vu une analogie de mon projet avec
un jeu d'échec.
Un article du blog de `Jean-François GAZET`_ au sujet la création d'un jeu d'échec m'a apporter quelques idées
pour mon développement. Il m'a aider à définir quelques éléments de l'implémentation, à savoir :
- l'IHM: mode console ou graphique ?
- la façon de représenter un élève, un plan de classe
- la notion de "critères" que j'ai requalifié de "contraintes" pour mon projet
- la stratégie algorithmique pour proposer une solution
Restait un élément à préciser : le format de données utilisé pour contenir la liste des élèves.
Mais ce dernier point n'était qu'une formalité.
IHM
===
J'ai tout de suite souhaité que mon application fonctionne en mode console pour deux raisons :
- la première :
Pour moi une application doit pouvoir fonctionner en mode console et pouvoir proposer
une interface graphique au besoin.
J'espérais ainsi passer la majeure partie du temps pour développer l'API et passer le reste sur
l'Interface-Homme-Machine.
- la seconde (qui est une conséquence de la première) :
Compte-tenu du délai qui m'était imposé je ne voulais pas passer trop de temps dans l'apprentissage
d'une librairie graphique.
Format de la liste des élèves
=============================
Afin de charger directement dans l'application une liste des élèves à manipuler,
il est nécessaire de choisir un format de fichier.
Le format **.csv** est celui retenu, de part sa simplicité à le générer.
Le séparateur de champ sera le point-virgule.
L'en-tête du ficher élèves est la suivante :
.. figure:: _static/figure2.png
:align: center
:scale: 60 %
Classes ``Student`` et ``SeatingPlan``
======================================
Dans le contexte de la classe, un **élève** est un individu portant certains attributs, comme
un nom, un prénom, une liste d'amis dans cette même classe ou à l'extérieur,...
Un **plan de classe** également peut porter certaines propriétés, comme ses dimensions,
la liste des sièges disponibles, leurs occupants,...
Ces deux éléments peuvent de surcroît interagir.
Il est alors apparu évident de considérer, d'un point de vue informatique, ces notions
comme des structures de données à part entière.
Ainsi leur définition, leur création, leur manipulation s'en trouveraient facilitées.
Dès lors, le choix du paradigme de programmation orientée objet a été décidé.
Deux classes, ont donc été définies pour cela :
- la classe ``Student``, pour définir les élèves
- la classe ``SeatingPlan``, pour définir le plan de classe
Student
-------
Un élève est défini par :
- son nom
- un niveau de bavardage
- une liste d'amis
::
def __init__(self, name, chat_lvl=0, friends=[]):
""" Instancie un objet plan de classe.
:param name:
Nom de l'élève.
:type name: str
:param chat_lvl:
Coefficient ou niveau de bavardage.
:type chat_lvl: int
:param friends:
Liste d'amis.
:type friends: list
"""
self.name = name
self.chat_lvl = chat_lvl
self.friends = friends
SeatingPlan
-----------
Un plan de classe est défini par :
- un nombre de rangées
- un nombre de colonnes
- une "cartographie" de la classe
À l'origine, la cartographie de mon plan de classe prenait la forme d'une liste
imbriquée, comme dans `jepychess`_.
Cette représentation a évolué pour une structure plus souple et surtout en adéquation
avec celle utilisée dans l'algorithme de calcul.
::
def __init__(self, row=5, col=8):
""" Instancie un objet plan de classe.
:param row:
Nombre de rangées dans la classe.
:type row: int
:param col:
Nombre de colonnes dans la classe.
:type col: int
:param mapping:
Représentation du plan de la classe.
:type mapping: dict
"""
self.row = row
self.col = col
self.mapping = {(i, j): None for i in range(row)
for j in range(col)}
Les contraintes
===============
Les contraintes sont les conditions suivant lesquelles on accepte que deux
élèves soient voisins dans le plan de classe.
Pour commencer, celles-ci seront "naïves". C'est-à-dire qu'elles seront basées
sur une valeur entière représentant le niveau de bavardage de l'élève.
Puis, elles sreont redéfinies pour prendre appui sur les relations sociales
entre les élèves (qui est ami avec ami).
Ainsi, deux élèves pourront être voisins si la différence entre leur niveaux
respectifs de bavardage est d'au moins une valeur fixée.
De la même manière, deux élèves pourront être voisins, si leur lien social
est minimal.
Les niveaux ou coefficients de bavardage seront définis comme suit :
.. figure:: _static/figure1.png
:align: center
:scale: 60 %
2nde phase de recherches : l'algorithme ?
=========================================
Balbutiements
-------------
Initialement, en me basant sur l'exemple de `xperprof`_, j'ai réalisé un placement
aléatoire sous contrainte, des élèves dans le plan de classe.
Le résultat fût concluant mais chaque lancement de cet algorithme produisait une seule proposition
qui pouvait ou non, être complète en raison de contraintes parfois trop fortes.
La première difficulté est ainsi apparue: satisfaire des contraintes tout en proposant des solutions possibles.
L'algorithme **alpha-beta** cité dans le blog de `Jean-François GAZET`_ m'a dirigé vers les méthode de résolution
de jeu d'échec, de dames ou plus généralement de problèmes NP-complet.
Mais également sur le fait de considérer les différentes étapes de recherche de complétion du plan de classe comme
un arbre de recherche.
Mes diverses lectures ainsi que des recherches sur l'Internet m'ont amené au problème des N-Reines.
Diverses méthodes de programmation permettent de résoudre ce problème : comme la programmation par contraintes.
Résoudre un problème de satisfaction de contraintes... ?
--------------------------------------------------------
J'ai vu la résolution de ma problématique comme un problème de satisfaction de contraintes et me suis mis en
quête de réponses concernant la programmation par contraintes.
Plusieurs possibilités
Je me suis alors tourné vers un algorithme avec retour sur trace.
L'algorithme au final
---------------------
Validateur de contraintes
"""""""""""""""""""""""""
::
def respect_constraints(self, seat, student, seatingplan, solution):
"""Indique si une place associée à une élève satisfait aux
contraintes, à savoir si :
- l'élève n'a pas déjà été positionné
- la place n'est pas déjà occupée
- la place est libre, que son voisinage est composé de bons voisins
en termes de bavardages.
:param seat:
Place que l'on veut associer à un élève.
:type seat: tuple
:param student:
Élève que l'on veut associer à une place.
:type student: Student
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:param solution:
Associations 'place - élève' valides.
:type solution: dict
:rtype: bool
.. todo::
- Modifier le traitement des contraintes de façon à tolérer
un certains nombre de voisins ne satisfaisants pas aux
contraintes.
- Ajouter les liens sociaux comme contraintes supplémentaires.
"""
# L'association en cours est supposée valide
result = True
# Si l'élève en cours a déjà été traité, l'association est invalide
if student in solution.values():
result = False
# return False
# Si la place en cours a déjà été traitée, l'association est invalide
elif seat in solution.keys():
result = False
# return False
else:
# Si non, veŕifions le voisinage de la place
# Récupérons le voisinage de la place en cours
neighbourhood = self.get_neighbourhood(seatingplan,
seat,
self.NEIGHBOURHOOD_RADIUS)
for neighbour_seat in neighbourhood:
# On ne teste les contraintes que sur les sièges déjà occupés
# donc sur ceux faisant déjà partie des solutions
if neighbour_seat in list(solution.keys()):
# On récupère l'élève du siège voisin
neighbour = solution[neighbour_seat]
# Lequel de l'élève en cours ou du voisin
# est le plus bavard ?
max_chat_lvl = max(student.chat_lvl,
neighbour.chat_lvl)
# Les contraintes portent sur la différence de
# coefficient de bavardage entre deux voisins:
if max_chat_lvl == 5:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_5)
elif max_chat_lvl == 4:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_4)
elif max_chat_lvl == 3:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_3)
elif max_chat_lvl == 2:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_2)
elif max_chat_lvl == 1:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_1)
else:
result = result and True
# Le siège voisin n'est pas occupé, il est donc valide
result = result and True
# retournons la validité de l'association
return result
Le solveur
""""""""""
::
def solve(self, seatingplan, students_list):
"""Cherche pour une liste de places et une liste d'élèves, un ensemble
de dispositions dans un plan de classe respectueuses de certaines
contraintes décrites dans la fonction 'respect_constraints' ci-dessus.
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:param students_list:
Liste d'élèves à positionner dans le plan de classe.
:type students_list: list
:param solution:
Solution de placement proposée.
:type solutions: dict
:return:
Disposition des élèves dans le plan de classe.
:rtype: generator
"""
# Liste des places
seats_list = list(seatingplan.mapping.keys())
# Quantités de places et d'élèves à traiter
number_of_seats = len(seats_list)
number_of_students = len(students_list)
# On stocke nos associations 'place - élève' dans un dictionnaire
solution = {}
# Indexes de départ dans nos listes de places et d'élèves
idx_seat = 0
idx_student = 0
# Retour sur trace (backtracking)
backtrack = False
# Fin du parcours
end = False
# On commence notre recherche
while not end:
# On ne revient pas encore en arrière
while not backtrack:
# Place à traiter
current_seat = seats_list[idx_seat]
# Élève à traiter
current_student = students_list[idx_student]
# L'association 'place - élève' est-elle satisfaisante ?
if self.respect_constraints(current_seat,
current_student,
seatingplan,
solution):
# Oui, alors associons l'élève à cette place
solution[current_seat] = current_student
# Est-ce qu'on a traité tous les élèves dans
# notre parcourt ?
if (idx_student == number_of_students-1):
# Oui, alors renvoyons notre solution
yield {p: s for p, s in solution.items()}
del solution[current_seat]
# A-t'on traité toutes les places ?
if (idx_seat != number_of_seats-1):
# Non, on passe à la suivante
idx_seat = idx_seat + 1
else:
# Oui, on revient en arrière
backtrack = True
else:
# Non, on passe au suivant en recommençant à la
# première place
idx_seat = 0
idx_student = idx_student + 1
# Les contraintes n'ont pas été satisfaites,
# il nous reste des places à traiter
elif (idx_seat != number_of_seats-1):
# On passe à la suivante
idx_seat = idx_seat + 1
else:
# Si non, on revient en arrière
backtrack = True
# Sommes-nous remonté jusqu'au premier élève ?
end = (idx_student == 0)
# On revient sur nos pas...
while (backtrack and not end):
# Prenons l'élève précédent
idx_student = idx_student - 1
current_student = students_list[idx_student]
# On récupère (et on enlève des solutions) la place qui lui
# est associée
current_seat = list(solution.keys())[list(solution.values())
.index(current_student)]
solution.pop(current_seat)
idx_seat = seats_list.index(current_seat)
# Il nous reste des places
if (idx_seat != number_of_seats-1):
# Prenons la place suivante et stoppons notre retour
# en arrière
idx_seat = idx_seat + 1
backtrack = False
# Ou nous sommes revenu au premier élève, on stop là
elif idx_student == 0:
end = True
********************************
Bilan et améliorations possibles
********************************
L'algorithme fonctionne bien dès lors que l'échantillon d'élèves n'est pas trop grand.
.. _xperprof: https://www.xperprof.fr/generateur_plan_de_classe
.. _Jean-François GAZET: https://fr.jeffprod.com/blog/2014/comment-programmer-un-jeu-dechecs.html
.. _jepychess: `Jean-François GAZET`_

9
docs/references.rst Normal file
View File

@ -0,0 +1,9 @@
**********
Références
**********
.. toctree::
:maxdepth: 1
:glob:
references/bibliography

View File

@ -0,0 +1,17 @@
*************
Bibliographie
*************
.. rubric::
.. [CAC+14] A. Cohen, J-P. Archambault, C. Cimelli, "Informatique et sciences du numérique - Edition spéciale Python !",
Eyrolles, 2014
.. [GRU17] J. Grus, "Data Science par la pratique : Fondamentaux avec Python",
Eyrolles, 2017
.. [SWI12] G. Swinnen, "Apprendre à programmer avec Python 3",
Eyrolles, 2012
.. [LeG14] V. Le Goff, "Apprenez à programmer en Python", 2e Édition,
OpenClassrooms - ex-Site du Zéro, 2014

View File

@ -0,0 +1,6 @@
***********
Webographie
***********
.. _xperprof: https://www.xperprof.fr/generateur_plan_de_classe
.. _Jean-François GAZET: https://fr.jeffprod.com/blog/2014/comment-programmer-un-jeu-dechecs.html

314
main.py Executable file
View File

@ -0,0 +1,314 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Name: main.py
Author: Jeff LANCE <jeff.lance@mala.fr>
Date: 29/03/2018
Générateur de plan de classe.
Cette application nécessite une liste d'élèves donnée sous forme d'un ficher
CSV d'en-tête "ID";"NAME";"CHAT LEVEL", avec:
ID: Numéro d'identification arbitraire de l'élève.
NAME: Nom de l'élève.
CHAT LEVEL: Coefficient de bavardage de l'élève.
"""
# Imports de modules
import random
from itertools import islice
from sage import SeatingPlan, Engine, interface
# Instanciation des objets
seatingplan = SeatingPlan()
engine = Engine()
# Fonctions du menu
def mainmenu_option1():
""" Option 1 du menu principal.
Redimensionne le plan de classe selon le nombre de rangées et de colonnes
demandés à l'utilisateur.
:return:
Rien.
"""
# on demande les nouvelles dimensions du plan de classe
row, col = interface.console_ask_seatingplan_size()
if row is not None and col is not None:
# on redimensionne le plan de classe
seatingplan.resize(row, col)
def mainmenu_option2():
"""Option 2 du menu principal.
Demande à l'utilisateur le fichier contenant la liste des élèves à
intégrer dans le plan de classe.
:return:
Rien ou la liste des élèves.
:rtype: list
"""
filepath = interface.dialog_ask_file()
# L'utilisateur a-t'il annulé sa recherche de fichier ?
if filepath:
# Non, on renvoie le fichier ouvert
dataframe = interface.open_file(filepath)
students_list = interface.load_users(dataframe, seatingplan)
return students_list
else:
# Oui
print('Opération annulée')
def mainmenu_option3(students_list):
"""Option 3 du menu principal.
Affiche la liste des élèves dans la console, si celle-ci n'est pas vide.
:param students_list:
Liste des élèves.
:type students_list: list
:return:
Rien.
"""
if students_list:
# on affiche la liste des élèves
interface.console_display_students(students_list)
else:
print("Votre liste d'élèves est vide")
def mainmenu_option4(students_list):
"""Option 4 du menu principal.
Demande à l'utilisateur l'identifiant d'un élève ainsi que les coordonnées
d'une place dans le plan de classe.
Positionne cet élève à la place indiquée.
:param students_list:
Liste des élèves.
:type students_list: list
:return:
Rien.
"""
# Si la liste contient des élèves
if students_list:
# On demande l'ID de l'élève
student_id = interface.console_ask_student_id()
# Si on a pu récupérer une valeur
if student_id is not None:
# On récupère l'élève associé à l'ID
student = students_list[student_id]
# On demande le siège où l'asseoir
student_seat = interface.console_ask_student_seat()
# Si on a pu récupérer une valeur
if seatingplan.is_a_seat(student_seat):
# On positionne l'élève
seatingplan.place_student(student, student_seat)
else:
print("Votre liste d'élèves est vide")
def mainmenu_option5(students_list):
"""Option 5 du menu principal.
Demande à l'utilisateur l'identifiant d'un élève.
Retire cet élève du plan de classe.
:param students_list:
Liste des élèves.
:type students_list: list
:return:
Rien.
"""
# Si la liste contient des élèves
if students_list:
# on demande l'ID de l'élève
student_id = interface.console_ask_student_id()
# Si on a pu récupérer une valeur
if student_id is not None:
# On récupère l'élève associé à l'ID
student = students_list[student_id]
# Et on le retire de sa place
seatingplan.remove_student(student)
else:
print("Votre liste d'élèves est vide")
def mainmenu_option6(students_list):
"""Option 6 du menu principal.
Demande à l'utilisateur les identifiants de deux élèves.
Échange les places de ces élèves.
:param students_list:
Liste des élèves.
:type students_list: list
:return:
Rien.
"""
# On demande l'ID du 1er élève
print('1er élève')
id1 = interface.console_ask_student_id()
# Si on a pu récupérer une valeur
if id1 is not None:
# On demande l'ID du 2nd élève
print('2ème élève')
id2 = interface.console_ask_student_id()
# Si on a pu récupérer une valeur
if id2 is not None:
# On échange les deux élèves de place
seatingplan.swap_students(students_list[id1], students_list[id2])
def mainmenu_option7(students_list):
"""Option 7 du menu principal.
Démarre une recherche des agencements possibles des élèves dans le plan
de classe.
:param students_list:
Liste des élèves.
:type students_list: list
:return:
Plan de classe possible.
:rtype: iterator
"""
number_of_proposals = input('Nombre maximal de propositions à faire : ')
number_of_proposals = int(number_of_proposals)
print('Presser Ctrl-C pour interrompre...')
try:
return iter(list(islice(engine.solve(seatingplan, students_list),
number_of_proposals)))
# return iter(list(engine.solve(seatingplan, students_list)))
except KeyboardInterrupt:
pass
else:
print('Appuyer sur "n" pour afficher la disposition suivante...')
def mainmenu_option8():
"""Option 8 du menu principal.
Efface le contenu du plan de classe courant.
:return: None
"""
print('Effacement du plan de classe...')
engine.flush_seatingplan(seatingplan)
def mainmenu_option_next(solution):
"""Option n du menu principal.
Affiche l'agencement de plan de classe possible suivant.
:param solution:
Agencement de plan de classe.
:type solution: dict
:return:
Rien.
"""
if solution:
try:
engine.flush_seatingplan(seatingplan)
engine.write_solution_to_seatingplan(next(solution), seatingplan)
except StopIteration:
print("Il n'y a plus de propositions de placement")
else:
print("Vous devez le calcul de solutions d'abord")
def settingsmenu():
""" Menu paramètres.
Affiche le menu paramètres dans la console.
:return:
Rien.
"""
while True:
interface.console_display_settingsmenu(engine)
command = input('>>> ')
if (command == 'b'):
return None
elif not command:
print('Choisissez une option')
else:
interface.console_set_engine_levels(engine, command)
def mainmenu():
""" Menu principal.
Affiche le menu principal dans la console.
:return:
Rien.
"""
while True:
# affichage de l'interface et du prompt
interface.console_display(seatingplan)
command = input('>>> ')
if (command == '1'):
mainmenu_option1()
elif (command == '2'):
students_list = mainmenu_option2()
elif (command == '3'):
mainmenu_option3(students_list)
elif (command == '4'):
mainmenu_option4(students_list)
elif (command == '5'):
mainmenu_option5(students_list)
elif (command == '6'):
mainmenu_option6(students_list)
elif (command == '7'):
proposals = mainmenu_option7(students_list)
elif (command == '8'):
mainmenu_option8()
elif (command == 'n'):
mainmenu_option_next(proposals)
elif (command == 'DEBUG'):
def f(s):
if s is not None:
return s.name
return s
# DEBUG: print solutions
for s in engine.solve(seatingplan, students_list):
print({p: f(s) for p, s in s.items()})
# DEBUG
elif (command == 's'):
settingsmenu()
elif (command == 'q'):
exit(0)
mainmenu()

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
colorclass==2.2.0
numpy==1.14.3
pandas==0.22.0
six==1.11.0
terminaltables==3.1.0

9
sage/__init__.py Normal file
View File

@ -0,0 +1,9 @@
"""
Name: __init__.py
Author: Jeff LANCE <jeff.lance@mala.fr>
Date: 29/03/2018
"""
from .student import Student
from .seatingplan import SeatingPlan
from .engine import Engine

383
sage/engine.py Normal file
View File

@ -0,0 +1,383 @@
"""
Name: engine.py
Author: Jeff LANCE <jeff.lance@mala.fr>
Date: 29/03/2018
Définition d'une classe Engine.
Contient les éléments intervenant dans les calculs de recherche de solutions.
"""
class Engine:
"""Le moteur de calculs qui travaille sur un plan de classe."""
def __init__(self):
"""Instancie un objet Engine.
:ivar NEIGHBORHOOD_RADIUS:
Rayon du voisinage.
:vartype NEIGHBORHOOD_RADIUS: int
:ivar DELTA_FOR_MAX_CHAT_LVL_1:
Différence minimale entre le coefficient de bavardage d'un élève et
ses voisins lorsque leur coefficient maximal vaut 1.
:vartype DELTA_FOR_MAX_CHAT_LVL_1: int
:ivar DELTA_FOR_MAX_CHAT_LVL_2:
Différence minimale entre le coefficient de bavardage d'un élève et
ses voisins lorsque leur coefficient maximal vaut 2.
:vartype DELTA_FOR_MAX_CHAT_LVL_32 int
:ivar DELTA_FOR_MAX_CHAT_LVL_3:
Différence minimale entre le coefficient de bavardage d'un élève et
ses voisins lorsque leur coefficient maximal vaut 3.
:vartype DELTA_FOR_MAX_CHAT_LVL_3: int
:ivar DELTA_FOR_MAX_CHAT_LVL_4:
Différence minimale entre le coefficient de bavardage d'un élève et
ses voisins lorsque leur coefficient maximal vaut 4.
:vartype DELTA_FOR_MAX_CHAT_LVL_4: int
:ivar DELTA_FOR_MAX_CHAT_LVL_5:
Différence minimale entre le coefficient de bavardage d'un élève et
ses voisins lorsque leur coefficient maximal vaut 5.
:vartype DELTA_FOR_MAX_CHAT_LVL_5: int
"""
self.NEIGHBOURHOOD_RADIUS = 1
self.DELTA_FOR_MAX_CHAT_LVL_1 = 0
self.DELTA_FOR_MAX_CHAT_LVL_2 = 1
self.DELTA_FOR_MAX_CHAT_LVL_3 = 1
self.DELTA_FOR_MAX_CHAT_LVL_4 = 2
self.DELTA_FOR_MAX_CHAT_LVL_5 = 2
def get_neighbourhood(self, seatingplan, seat, radius):
"""Dans un plan de classe, retourne le voisinage dans un rayon donné
d'une place.
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:param seat:
Coordonnées de la place dans le plan de classe.
:type seat: tuple
:param radius:
Rayon du voisinage.
:type radius: int
:return:
Liste des places voisines.
:rtype: list
"""
# La liste des voisins
neighbourhood = []
# 'position' est un tuple de coordonnées (i, j) où:
# - i compris entre 0 et seatingplan.row-1
# - j compris entre 0 et seatingplan.col-1
#
# Les voisins sont les élèves aux places de coordonnées situées
# dans le carré passant par les points suivants :
# (i, j-radius), (i, j+radius), (i-radius, j), (i+radius, j),
# (i-radius, j-radius), (i-radius, j+radius),
# (i+radius, j-radius) et (i+radius, j+radius)
# On analyse ce voisinage et on n'ajoute un voisin que s'il y en a un
for i in range(-radius, radius+1, 1):
for j in range(-radius, radius+1, 1):
# Traitons la place voisine
neighbour_seat = (seat[0]+i, seat[1]+j)
# print("Place: ", neighbour_seat) # DEBUG #
# Cette place est-elle ?
# - différente de la place autour de laquelle on regarde
# - bien comprise dans le plan de classe (coordonnées de
# dépassant pas celles du plan de classe)
if (neighbour_seat != seat and
0 <= neighbour_seat[0] <= seatingplan.row-1 and
0 <= neighbour_seat[1] <= seatingplan.col-1):
neighbourhood.append(neighbour_seat)
# On retourne notre liste de places voisines
return neighbourhood
def are_safe_neighbours(self, student, neighbour, delta_chat_lvl):
"""Indique si deux élèves constituent de bons voisins.
C'est-à-dire si la différence de leur coefficient de bavardage
est d'une certaine valeur.
:param student:
Élève.
:type student: Student
:param neighbour:
Élève.
:type neighbour: Student
:param delta_chat_lvl:
Différence entre les coefficients de bavardage.
:type delta_chat_lvl: int
:rtype: bool
"""
return bool(abs(student.chat_lvl - neighbour.chat_lvl)
>= delta_chat_lvl)
def respect_constraints(self, seat, student, seatingplan, solution):
"""Indique si une place associée à une élève satisfait aux
contraintes, à savoir si :
- l'élève n'a pas déjà été positionné
- la place n'est pas déjà occupée
- la place est libre, que son voisinage est composé de bons voisins
en termes de bavardages.
:param seat:
Place que l'on veut associer à un élève.
:type seat: tuple
:param student:
Élève que l'on veut associer à une place.
:type student: Student
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:param solution:
Associations 'place - élève' valides.
:type solution: dict
:rtype: bool
.. todo::
- Modifier le traitement des contraintes de façon à tolérer
un certains nombre de voisins ne satisfaisants pas aux
contraintes.
- Ajouter les liens sociaux comme contraintes supplémentaires.
"""
# L'association en cours est supposée valide
result = True
# Si l'élève en cours a déjà été traité, l'association est invalide
if student in solution.values():
result = False
# return False
# Si la place en cours a déjà été traitée, l'association est invalide
elif seat in solution.keys():
result = False
# return False
else:
# Si non, veŕifions le voisinage de la place
# Récupérons le voisinage de la place en cours
neighbourhood = self.get_neighbourhood(seatingplan,
seat,
self.NEIGHBOURHOOD_RADIUS)
for neighbour_seat in neighbourhood:
# On ne teste les contraintes que sur les sièges déjà occupés
# donc sur ceux faisant déjà partie des solutions
if neighbour_seat in list(solution.keys()):
# On récupère l'élève du siège voisin
neighbour = solution[neighbour_seat]
# Lequel de l'élève en cours ou du voisin
# est le plus bavard ?
max_chat_lvl = max(student.chat_lvl,
neighbour.chat_lvl)
# Les contraintes portent sur la différence de
# coefficient de bavardage entre deux voisins:
if max_chat_lvl == 5:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_5)
elif max_chat_lvl == 4:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_4)
elif max_chat_lvl == 3:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_3)
elif max_chat_lvl == 2:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_2)
elif max_chat_lvl == 1:
result = result and self.are_safe_neighbours(
student,
neighbour,
self.DELTA_FOR_MAX_CHAT_LVL_1)
else:
result = result and True
# Le siège voisin n'est pas occupé, il est donc valide
result = result and True
# retournons la validité de l'association
return result
def solve(self, seatingplan, students_list):
"""Cherche pour une liste de places et une liste d'élèves, un ensemble
de dispositions dans un plan de classe respectueuses de certaines
contraintes décrites dans la fonction 'respect_constraints' ci-dessus.
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:param students_list:
Liste d'élèves à positionner dans le plan de classe.
:type students_list: list
:param solution:
Solution de placement proposée.
:type solutions: dict
:return:
Disposition des élèves dans le plan de classe.
:rtype: generator
"""
# Liste des places
seats_list = list(seatingplan.mapping.keys())
# Quantités de places et d'élèves à traiter
number_of_seats = len(seats_list)
number_of_students = len(students_list)
# On stocke nos associations 'place - élève' dans un dictionnaire
solution = {}
# Indexes de départ dans nos listes de places et d'élèves
idx_seat = 0
idx_student = 0
# Retour sur trace (backtracking)
backtrack = False
# Fin du parcours
end = False
# On commence notre recherche
while not end:
# On ne revient pas encore en arrière
while not backtrack:
# Place à traiter
current_seat = seats_list[idx_seat]
# Élève à traiter
current_student = students_list[idx_student]
# L'association 'place - élève' est-elle satisfaisante ?
if self.respect_constraints(current_seat,
current_student,
seatingplan,
solution):
# Oui, alors associons l'élève à cette place
solution[current_seat] = current_student
# Est-ce qu'on a traité tous les élèves dans
# notre parcourt ?
if (idx_student == number_of_students-1):
# Oui, alors renvoyons notre solution
yield {p: s for p, s in solution.items()}
del solution[current_seat]
# A-t'on traité toutes les places ?
if (idx_seat != number_of_seats-1):
# Non, on passe à la suivante
idx_seat = idx_seat + 1
else:
# Oui, on revient en arrière
backtrack = True
else:
# Non, on passe au suivant en recommençant à la
# première place
idx_seat = 0
idx_student = idx_student + 1
# Les contraintes n'ont pas été satisfaites,
# il nous reste des places à traiter
elif (idx_seat != number_of_seats-1):
# On passe à la suivante
idx_seat = idx_seat + 1
else:
# Si non, on revient en arrière
backtrack = True
# Sommes-nous remonté jusqu'au premier élève ?
end = (idx_student == 0)
# On revient sur nos pas...
while (backtrack and not end):
# Prenons l'élève précédent
idx_student = idx_student - 1
current_student = students_list[idx_student]
# On récupère (et on enlève des solutions) la place qui lui
# est associée
current_seat = list(solution.keys())[list(solution.values())
.index(current_student)]
solution.pop(current_seat)
idx_seat = seats_list.index(current_seat)
# Il nous reste des places
if (idx_seat != number_of_seats-1):
# Prenons la place suivante et stoppons notre retour
# en arrière
idx_seat = idx_seat + 1
backtrack = False
# Ou nous sommes revenu au premier élève, on stop là
elif idx_student == 0:
end = True
def write_solution_to_seatingplan(self, solution, seatingplan):
"""Place les élèves de la solution dans un plan de classe.
:param solution:
Solution de positionnement des élèves dans le plan de classe.
:type solution: dict
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:return: None
"""
for place, student in solution.items():
if student is not None:
seatingplan.place_student(student, place)
else:
seatingplan.mapping[place] = None
def flush_seatingplan(self, seatingplan):
"""Vide le plan de classe. Supprime tous les élèves placés.
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:return: None
"""
for seat in list(seatingplan.mapping.keys()):
student = seatingplan.get_student(seat)
if student is not None:
seatingplan.remove_student(student)
def get_corners(self, seatingplan):
""" Renvoie, pour un plan de classe, la liste des places aux coins
ou extrémités.
C'est-à-dire une place située en : (0,0), (0,m-1),
(n-1,0) ou (n-1,m-1) pour un plan de classe de dimensions n x m.
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:return:
Liste des places aux coins/extrémités.
:rtype: list
"""
# Dimensions du plan de classe
n = seatingplan.raw
m = seatingplan.col
# Le plan de classe est un tableau de dimensions n x m :
# - si n>1 et m>1, il possède 4 coins
# - si n=1 et m>1, il en possède 2
# - si n>1 et m=1, il en possède 2
# - si n=1 et m=1, il en possède 1
#
# On utilise donc un set qui va permettre d'éliminer les doublons de
# coordonnées
# Ce set est ensuite "traduit" en liste
corners = list({(0, 0), (n-1, 0), (0, m-1), (n-1, m-1)})
return corners

489
sage/interface.py Normal file
View File

@ -0,0 +1,489 @@
"""
Name: interface.py
Author: Jeff LANCE <jeff.lance@mala.fr>
Date: 29/03/2018
Module d'interface de l'application.
Contient les éléments permettant de communiquer avec l'utilisateur.
"""
# Import de modules
import os
from tkinter import filedialog
from tkinter import *
import pandas as pd
from pandas import errors
from terminaltables import AsciiTable
from colorclass import Color
from .student import Student
#########################
# Interface application #
#########################
def console_display(seatingplan):
"""Affiche le menu et le plan de classe dans la console.
:param seatingplan:
Plan de classe à afficher.
:type seatingplan: SeatingPlan
:return:
Rien.
"""
console_display_mainmenu(seatingplan)
console_display_seatingplan(seatingplan)
#########
# Menus #
#########
def console_display_mainmenu(seatingplan):
"""Affiche le menu principal de l'application dans la console.
:param seatingplan:
Plan de classe à afficher.
:type seatingplan: SeatingPlan
:return:
Rien.
"""
input('\nAppuyer sur une touche pour continuer...')
# On efface l'écran (cls: win, clear: unix)
os.system('cls' if os.name == 'nt' else 'clear')
print('')
print('Bienvenue !')
print('')
print("=> Entrez le numéro de l'action à effectuer :")
print('- 1. pour définir les dimensions du plan de classe: {}x{} '
'actuellement'.format(seatingplan.row, seatingplan.col))
print("- 2. pour ouvrir et charger une liste d'élèves depuis "
'un fichier .csv')
print('- 3. pour afficher la liste des élèves chargés précédemment')
print("- 4. pour placer un élève à une place précise:\n"
' • la place en bas à gauche a pour coordonnées (1,1)\n'
' • la place en haut à droite a pour coordonnées ({},{})\n'
' (nécessite son ID)'
.format(seatingplan.row, seatingplan.col))
print("- 5. pour enlever un élève du plan de classe (nécessite son ID)")
print('- 6. pour intervertir les places de deux élèves (nécessite les ID)')
print('- 7. pour obtenir une liste de plans de classe possibles')
print('- 8. pour vider le plan de classe')
print('')
print("- s. pour paramétrer l'application")
print('- q. pour quitter')
print('')
def console_display_settingsmenu(engine):
"""Affiche le menu de paramétrage du de l'application dans la console.
:param engine:
Moteur de calcul.
:type engine: Engine
:return:
Rien.
"""
input('\nAppuyer sur une touche pour continuer...')
# On efface l'écran (cls: win, clear: unix)
os.system('cls' if os.name == 'nt' else 'clear')
print('')
print('Paramètres actuels')
print('')
print('Delta pour coefficient de bavardage\n'
'Max 1 = {} | Max 2 = {} | Max 3 = {} | Max 4 = {} | Max 5 = {}'
.format(engine.DELTA_FOR_MAX_CHAT_LVL_1,
engine.DELTA_FOR_MAX_CHAT_LVL_2,
engine.DELTA_FOR_MAX_CHAT_LVL_3,
engine.DELTA_FOR_MAX_CHAT_LVL_4,
engine.DELTA_FOR_MAX_CHAT_LVL_5))
print('')
print('- Entrez le numéro du niveau à ajuster\n')
print('- b. pour revenir au menu principal')
print('')
##################
# Plan de classe #
##################
def console_display_seatingplan(seatingplan):
"""Affiche le plan de classe dans la console.
:param seatingplan:
Plan de classe à afficher.
:type seatingplan: SeatingPlan
:return:
Rien.
"""
# On transforme le plan de classe en table
rendered_seatingplan = _seatingplan_to_table(seatingplan)
# Quelques ajustements visuels...
for i in range(seatingplan.col):
rendered_seatingplan.justify_columns[i] = 'center'
rendered_seatingplan.padding_left = 2
rendered_seatingplan.padding_right = 2
rendered_seatingplan.inner_heading_row_border = False
rendered_seatingplan.inner_row_border = True
# On affiche notre jolie tableau
print(rendered_seatingplan.table, '\n')
def _seatingplan_to_table(seatingplan):
"""Embellit le plan de classe pour affichage.
:param seatingplan:
Plan de classe à afficher.
:type seatingplan: SeatingPlan
:return:
Plan de classe sous forme de tableau.
:rtype: AsciiTable
"""
# On crée un tableau vide de dimensions égales à celle du plan de classe
seatingplan_as_table = [[None for j in range(seatingplan.col)]
for i in range(seatingplan.row)]
# Parcourons le plan de classe
for place in seatingplan.mapping:
# On récupère les coordonnées de la place en cours...
x, y = place[0], place[1]
# ...on prend son contenu que l'on formate (coloration,...)
content = _console_render_student(seatingplan.get_student(place))
# Si la place est occupée
if not seatingplan.is_empty_seat(place):
# On rajoute des informations au contenu (id, chat level)
addon = 'I: {} - L: {}' \
.format(seatingplan.get_student(place).id,
seatingplan.get_student(place).chat_lvl)
content = content + '\n' + addon
# On place le contenu dans notre table
seatingplan_as_table[seatingplan.row-1-x][y] = content
# On transforme et on renvoit notre table en tant qu'objet formaté
# à l'aide du modul terminaltables
return AsciiTable(seatingplan_as_table)
###################
# Options du menu #
###################
#
# Option 1
#
def console_ask_seatingplan_size():
"""Demande à l'utilisateur de définir la taille du plan de classe.
:return:
Dimensions du plan de classe.
:rtype: tuple or None
:raises: ValueError
"""
# On demande le nombre de rangées
# row va contenir une chaîne de caractère
row = input('\nVeuillez entrer le nombre de rangées: ')
# La valeur entrée est-elle valide (un entier > 0) ?
try:
# row est convertit en entier, sinon on intercepte une exception
row = int(row)
# Si pas d'exception...
# si row est négative ou nulle, on lève une exception
if row <= 0:
raise ValueError('Le numéro de rangées doit être strictement \
supérieur à zéro')
# Autrement tout va bien, on passe aunombre de colonnes
col = input('\nVeuillez entrer le nombre de colonnes: ')
# La valeur entrée est-elle valide (un entier > 0) ?
try:
# col est convertit en entier, sinon on intercepte une exception
col = int(col)
# Si pas d'exception...
# si col est négative ou nulle, on lève une exception
if col <= 0:
raise ValueError('Le numéro de colonnes doit être strictement \
supérieur à zéro')
# Interception de l'exception pour col
except ValueError:
print('La valeur de colonne est invalide')
col = None
# Interception de l'exception pour row
except ValueError:
print('La valeur de rangée est invalide')
row, col = None, None
return row, col
#
# Option 2
#
def dialog_ask_file():
"""Affiche une boite de dialogue permettant de sélectionner
un fichier csv contenant la liste des utilisateurs à affecter
dans le plan de classe.
:return:
Chemin absolu du fichier sélectionné.
:rtype: str
"""
root = Tk()
# On ne veut pas voir la fenêtre Tkinter
root.withdraw()
# Affiche une boite de dialogue demandant de sélectionner un fichier
# Renvoie le chemin du path qui est stocké dans 'filename'
filepath = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
return filepath
def open_file(filename):
""" Ouvre un fichier de données csv.
:param filename:
Chemin du fichier à ouvrir.
:type filename: str
:return:
DataFrame pandas
:rtype: dict
:raises: pd.errors.EmptyDataError
"""
# On gère l'erreur liée au fait que le fichier soit vide
try:
dataframe = pd.read_csv(filename, sep=';',
header=0, index_col=None).to_dict('records')
print('Ouverture du fichier:', filename)
return dataframe
except pd.errors.EmptyDataError:
print('Fichier vide')
def load_users(dataframe, seatingplan):
"""Instancie des élèves à partir d'un dataframe.
Les objets 'student' sont instanciés à partir du dictionnaire
'dataframe', contenant :
- un id(entifiant) pour l'élève
- nom de l'élève
- niveau de bavardage de l'élève
:param dataframe:
"Liste" d'élèves.
:type dataframe: dict
:param seatingplan:
Plan de classe.
:type seatingplan: SeatingPlan
:return:
Liste d'élèves instanciés.
:rtype: list
"""
students_list = []
for i, user in enumerate(dataframe):
students_list.append(Student(user['NAME']))
students_list[i].id = user['ID']
students_list[i].chat_lvl = user['CHAT LEVEL']
return students_list
#
# Option 3
#
def console_display_students(students_list):
"""Affiche la liste des élèves dans la console.
:param students_list:
Liste d'élèves.
:type students_list: list
:return: None
"""
students_table = [['ID', 'NAME', 'CHAT LEVEL']]
for student in students_list:
students_table.append([student.id,
_console_render_student(student),
student.chat_lvl])
rendered_students_table = AsciiTable(students_table)
print(rendered_students_table.table)
def _console_render_student(student):
"""Formate le nom de l'élève 'student' pour l'affichage en mode
console du plan de classe.
:param student:
Élève.
:type student: Student
:return:
Nom de l'élève en couleur.
:rtype: str
"""
name = '.'
# Est-on bien en présence d'un élève (objet Student) et
# pas d'une place vide (None) ?
if isinstance(student, Student):
# En fonction du coefficient de bavardage de l'élève, on colorie son
# nom
if 0 <= student.chat_lvl <= 1:
name = Color('{green}' + student.name + '{/green}')
elif 2 <= student.chat_lvl <= 3:
name = Color('{yellow}' + student.name + '{/yellow}')
elif 4 <= student.chat_lvl <= 5:
name = Color('{red}' + student.name + '{/red}')
#
# On remplace l'espcace entre le nom et le prénom par un saut de ligne
# "incompatible" avec la coloration: provoque une coloration de ligne
# name = student.name.replace(' ', '\n', 1)
#
return name
#
# Option 4 et 5
#
def console_ask_student_id():
"""Demande à l'utilisateur l'identifiant d'un élève.
:return:
Identifiant d'un élève.
:rtype: int
:raises: ValueError
"""
id = input("Veuillez entrer l'ID de l'élève: ")
# La valeur entrée est-elle un entier > 0 ?
try:
# On s'assure que la valeur entrée est entière en la convertissant
# sinon, on intercepte une exception
id = int(id)
# Si la valeur est entière mais négative stricte
if id < 0:
# On lève une exception
raise ValueError("La valeur d'ID doit être supérieure ou égale "
"à zéro")
except ValueError:
print('La valeur saisie est invalide')
else:
return id
#
# Option 4
#
def console_ask_student_seat():
"""Demande à l'utilisateur les coordonnées d'une place
dans le plan de classe.
:return:
Coordonnées d'une place.
:rtype: tuple
:raises: ValueError
"""
student_row = input('\nVeuillez entrer le numéro de rangée où vous'
" souhaitez placer l'élève: ")
# La valeur entrée est-elle un entier > 0 ?
try:
# On s'assure que la valeur entrée est entière en la convertissant
# sinon, on intercepte une exception
student_row = int(student_row)
# Si la valeur est entière mais négative
if student_row <= 0:
# On lève une exception
raise ValueError('Le numéro de rangée doit être strictement '
'positif')
# Tout va bien, on passe au numéro de colonne
student_col = input('\nVeuillez entrer le numéro de colonne où vous'
" souhaitez placer l'élève: ")
# La valeur entrée est-elle un entier > 0 ?
try:
# On s'assure que la valeur entrée est entière en la convertissant
# sinon, on intercepte une exception
student_col = int(student_col)
# Si la valeur est entière mais négative
if student_col <= 0:
# On lève une exception
raise ValueError('Le numéro de colonne doit être strictement '
'positif')
except ValueError:
print('La valeur saisie pour le numéro de colonne est invalide')
student_col = None
except ValueError:
print('La valeur saisie pour le numéro de rangée est invalide')
student_row, student_col = None, None
# On renvoit les valeurs entrées mais ajustées à l'indexation
# du plan de classe
return student_row, student_col
#
# Option s
#
def console_set_engine_levels(engine, level):
"""Option Level du menu paramètres.
Propose à l'utilisateur de modifier les contraintes sur les niveaux de
bavardage.
:param engine:
Moteur de calculs.
:type engine: Engine
:param level:
Niveau à modifier.
:type level: int
:return:
Rien.
"""
# La valeur passée en paramètres est-elle un entier compris entre 1 et 5 ?
try:
# On s'assure que la valeur entrée est bien entière
level = int(level)
# Si elle n'est pas comprise entre 1 et 5
if (level < 1 or level > 5):
# On lève une exception
raise ValueError('La valeur doit être comprise entre 1 et 5')
# Autrement, on continue en demandant la valeur du coefficient
coeff = input('Ajuster coefficient maximal {}: '.format(level))
# Cette valeur est-elle un entier positif ou nul ?
try:
# On s'assure que la valeur entrée est bien entière
coeff = int(coeff)
# Si elle est strictement négative
if coeff < 0:
# On lève une exception
raise ValueError('La valeur doit être positive ou nulle')
except ValueError:
print('La valeur saisie est invalide')
else:
print(coeff)
if level == 1:
engine.DELTA_FOR_MAX_CHAT_LVL_1 = coeff
elif level == 2:
engine.DELTA_FOR_MAX_CHAT_LVL_2 = coeff
elif level == 3:
engine.DELTA_FOR_MAX_CHAT_LVL_3 = coeff
elif level == 4:
engine.DELTA_FOR_MAX_CHAT_LVL_4 = coeff
elif level == 5:
engine.DELTA_FOR_MAX_CHAT_LVL_5 = coeff
except ValueError:
print('Saisie est invalide')

205
sage/seatingplan.py Normal file
View File

@ -0,0 +1,205 @@
"""
Name: seatingplan.py
Author: Jeff LANCE <jeff.lance@mala.fr>
Date: 29/03/2018
Définition d'une classe 'Plan de classe'.
"""
class SeatingPlan:
"""Plan de classe.
Est un ensemble de places disposées suivant un nombre donné de rangées et
de colonnes.
"""
def __init__(self, row=5, col=8):
"""Instancie un objet plan de classe.
:param row:
Nombre de rangées dans la classe.
:type row: int
:param col:
Nombre de colonnes dans la classe.
:type col: int
:param mapping:
Représentation du plan de la classe sous la forme
{(place): (student)}
:type mapping: dict
"""
self.row = row
self.col = col
self.mapping = {(i, j): None for i in range(row)
for j in range(col)}
def is_full(self):
"""Renvoie si le plan de classe est plein ou non.
:return:
True s'il ne reste aucune place vide. False sinon.
:rtype: bool
"""
return None in self.mapping.values()
def resize(self, row, col):
"""Redimensionne le plan de classe.
:param row:
Nombre de rangées.
:type row: int
:param col:
Nombre de colonnes.
:type row: int
"""
self.row = row
self.col = col
self.mapping = {(i, j): None for i in range(row)
for j in range(col)}
def is_a_seat(self, seat):
"""Renvoie si une place est valide ou non.
Vérifie si les coordonnées de la place sont valides. C'est-à-dire
si chacune des coordonnées n'est pas vide et si la place fait bien
partie du plan de classe.
:param seat:
Place.
:type seat: tuple
:return:
True, si la place est valide et appartient au plan de classe.
False, si non.
:rtype: bool
"""
# On suppose par défaut que la place est valide
valid = True
# Si les coordonnées sont valides
if (seat[0] is not None and seat[1] is not None):
# Si le siège n'est pas dans le plan de classe
if seat not in self.mapping:
# La place n'est pas valide
valid = False
# Si les coordonnées ne le sont pas, la place non plus
else:
valid = False
# On retourne la validité
return valid
def get_student(self, seat):
"""Renvoie l'élève assis à une place.
:param seat:
Coordonnées de la place dans le plan de classe.
:type seat: tuple
:return: None si personne à cette place.
Élève si la place est occupée.
:rtype: Student ou None
"""
if self.is_a_seat(seat):
return self.mapping[seat]
def get_seat(self, student):
"""Renvoie la place d'un élève.
:param student:
Élève.
:type student: Student
:return:
La place de l'élève dans le plan de classe.
False si l'élèves n'est pas dans le plan de classe.
:rtype: tuple or False
"""
try:
return list(self.mapping.keys())[list(self.mapping.values())
.index(student)]
except ValueError:
return False
def is_empty_seat(self, seat):
"""Indique si une place est libre.
:param seat:
Coordonnées de la place dans le plan de classe.
:type seat: tuple
:return:
True si la place est vide, False sinon.
:rtype: bool
"""
if self.is_a_seat(seat):
return self.get_student(seat) is None
return False
def place_student(self, student, seat):
"""Positionne un élève dans le plan de classe.
:param student:
Élève à placer.
:type student: Student
:param seat:
Place à laquelle asseoir l'élève.
:type seat: tuple
:return:
True, si la place est libre et que le positionnement s'est
bien déroulé.
False, si la place est occupée.
:rtype: bool
"""
# On peut placer l'élève a une place si celle-ci est vide
if self.is_empty_seat(seat):
# On place l'élève
self.mapping[seat] = student
return True
return False
def remove_student(self, student):
"""Retire un élève de sa place.
:param student:
Élève à retirer.
:type student: Student
:return:
True si la suppression s'est bien déroulée, False sinon.
:rtype: bool
"""
# On rećupère la place de l'élève
seat = self.get_seat(student)
if seat:
# Le siège dans le plan de classe est libéré
self.mapping[seat] = None
return True
return False
def swap_students(self, student_one, student_two):
"""Fait s'échanger de place deux élèves.
:param student_one:
Premier élève.
:type student_one: Student
:param student_two:
Deuxième élève.
:type student_one: Student
:return:
Si l'opération s'est bien déroulée.
:rtype: bool
"""
# On récupère leur place
seat_one = self.get_seat(student_one)
seat_two = self.get_seat(student_two)
# On les retire
if (self.remove_student(student_one)
and self.remove_student(student_two)):
# On attribue à chacun sa nouvelle place
if (self.place_student(student_one, seat_two)
and self.place_student(student_two, seat_one)):
return True
return False

34
sage/student.py Normal file
View File

@ -0,0 +1,34 @@
"""
Name: student.py
Author: Jeff LANCE <jeff.lance@mala.fr>
Date: 29/03/2018
Définiton d'une classe 'Student'.
Contient les éléments néćessaire à la création, manipulation d'élèves.
"""
class Student:
"""Élève.
Un élève d'une classe a pour attributs: un nom, un niveau de bavardage,
une liste d'amis.
"""
def __init__(self, name, chat_lvl=0, friends=[]):
"""Instancie un objet plan de classe.
:param name:
Nom de l'élève.
:type name: str
:param chat_lvl:
Coefficient ou niveau de bavardage.
:type chat_lvl: int
:param friends:
Liste d'amis.
:type friends: list
"""
self.name = name
self.chat_lvl = chat_lvl
self.friends = friends

1
tests/test_empty.csv Normal file
View File

@ -0,0 +1 @@
"Id";"Name";"Chat level"
1 Id Name Chat level

0
tests/test_fullempty.csv Normal file
View File

View File

@ -0,0 +1,2 @@
"Id";"Name"
0;"Jean AIMAR";4
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -0,0 +1,8 @@
;0;1;2;3;4;5;6
0;0;0;1;1;1;1;0
1;0;0;0;0;1;0;1
2;1;0;0;0;1;1;0
3;1;0;0;0;1;1;1
4;1;1;1;1;0;1;0
5;1;0;1;1;1;0;0
6;0;1;0;1;0;0;0
1 0 1 2 3 4 5 6
2 0 0 0 1 1 1 1 0
3 1 0 0 0 0 1 0 1
4 2 1 0 0 0 1 1 0
5 3 1 0 0 0 1 1 1
6 4 1 1 1 1 0 1 0
7 5 1 0 1 1 1 0 0
8 6 0 1 0 1 0 0 0

13
tests/user_table_12.csv Normal file
View File

@ -0,0 +1,13 @@
"ID";"NAME";"CHAT LEVEL"
0;"Jean NEYMAR";1
1;"Sam DÉPASSE";0
2;"Elsa DORSA";2
3;"Sophie STICKÉ";3
4;"Terry GOLO";4
5;"Marie HONETTE";1
6;"Jérémy TOUTENPLACE";3
7;"Yvon PARTIR";5
8;"Anne HANNA";2
9;"Théo KAY";1
10;"Marin DODOUCE";4
11;"Pierre KIROULE";5
1 ID NAME CHAT LEVEL
2 0 Jean NEYMAR 1
3 1 Sam DÉPASSE 0
4 2 Elsa DORSA 2
5 3 Sophie STICKÉ 3
6 4 Terry GOLO 4
7 5 Marie HONETTE 1
8 6 Jérémy TOUTENPLACE 3
9 7 Yvon PARTIR 5
10 8 Anne HANNA 2
11 9 Théo KAY 1
12 10 Marin DODOUCE 4
13 11 Pierre KIROULE 5

16
tests/user_table_15.csv Normal file
View File

@ -0,0 +1,16 @@
"ID";"NAME";"CHAT LEVEL"
0;"Jean NEYMAR";1
1;"Sam DÉPASSE";0
2;"Elsa DORSA";2
3;"Sophie STICKÉ";3
4;"Terry GOLO";4
5;"Marie HONETTE";1
6;"Jérémy TOUTENPLACE";3
7;"Yvon PARTIR";5
8;"Anne HANNA";2
9;"Théo KAY";1
10;"Marin DODOUCE";4
11;"Pierre KIROULE";5
12;"Agathe ZEUBLOUZE";1
13;"Ahmed AYPAN";3
14;"Élie KOPTER";0
1 ID NAME CHAT LEVEL
2 0 Jean NEYMAR 1
3 1 Sam DÉPASSE 0
4 2 Elsa DORSA 2
5 3 Sophie STICKÉ 3
6 4 Terry GOLO 4
7 5 Marie HONETTE 1
8 6 Jérémy TOUTENPLACE 3
9 7 Yvon PARTIR 5
10 8 Anne HANNA 2
11 9 Théo KAY 1
12 10 Marin DODOUCE 4
13 11 Pierre KIROULE 5
14 12 Agathe ZEUBLOUZE 1
15 13 Ahmed AYPAN 3
16 14 Élie KOPTER 0

17
tests/user_table_16.csv Normal file
View File

@ -0,0 +1,17 @@
"ID";"NAME";"CHAT LEVEL"
0;"Jean NEYMAR";1
1;"Sam DÉPASSE";0
2;"Elsa DORSA";2
3;"Sophie STICKÉ";3
4;"Terry GOLO";4
5;"Marie HONETTE";1
6;"Jérémy TOUTENPLACE";3
7;"Yvon PARTIR";5
8;"Anne HANNA";2
9;"Théo KAY";1
10;"Marin DODOUCE";4
11;"Pierre KIROULE";5
12;"Agathe ZEUBLOUZE";1
13;"Ahmed AYPAN";3
14;"Élie KOPTER";0
15;"Nordine ATEUR";3
1 ID NAME CHAT LEVEL
2 0 Jean NEYMAR 1
3 1 Sam DÉPASSE 0
4 2 Elsa DORSA 2
5 3 Sophie STICKÉ 3
6 4 Terry GOLO 4
7 5 Marie HONETTE 1
8 6 Jérémy TOUTENPLACE 3
9 7 Yvon PARTIR 5
10 8 Anne HANNA 2
11 9 Théo KAY 1
12 10 Marin DODOUCE 4
13 11 Pierre KIROULE 5
14 12 Agathe ZEUBLOUZE 1
15 13 Ahmed AYPAN 3
16 14 Élie KOPTER 0
17 15 Nordine ATEUR 3

37
tests/user_table_36.csv Normal file
View File

@ -0,0 +1,37 @@
"ID";"NAME";"CHAT LEVEL"
0;"Jean NEYMAR";1
1;"Sam DÉPASSE";0
2;"Elsa DORSA";2
3;"Sophie STICKÉ";3
4;"Terry GOLO";4
5;"Marie HONETTE";1
6;"Jérémy TOUTENPLACE";3
7;"Yvon PARTIR";5
8;"Anne HANNA";2
9;"Théo KAY";1
10;"Marin DODOUCE";4
11;"Pierre KIROULE";5
12;"Agathe ZEUBLOUZE";1
13;"Ahmed AYPAN";3
14;"Élie KOPTER";0
15;"Nordine ATEUR";3
16;"Youri LIGOTMI";5
17;"Robin DIDON";5
18;"Kader HUSSELLE";2
19;"Paul AUCHON";3
20;"Ali ABALDAKIN";4
21;"Annie CROCHE";1
22;"Fabien GAFATOY";3
23;"Lydie AUDUVILAGE";5
24;"Maud TETTE";1
25;"Medhi KAMAN";2
26;"Sarah VIGOTTE";4
27;"Walid TONTIKAY";5
28;"Yann NIVERSERT";1
29;"Youssouf DANLBALON";3
30;"Aude JAVEL";0
31;"Emma KARENA";0
32;"Gilbert MUDA";1
33;"Thibault LONIAISE";5
34;"Ève ANOUI";5
35;"Mélanie SANDANLGARAJE";5
1 ID NAME CHAT LEVEL
2 0 Jean NEYMAR 1
3 1 Sam DÉPASSE 0
4 2 Elsa DORSA 2
5 3 Sophie STICKÉ 3
6 4 Terry GOLO 4
7 5 Marie HONETTE 1
8 6 Jérémy TOUTENPLACE 3
9 7 Yvon PARTIR 5
10 8 Anne HANNA 2
11 9 Théo KAY 1
12 10 Marin DODOUCE 4
13 11 Pierre KIROULE 5
14 12 Agathe ZEUBLOUZE 1
15 13 Ahmed AYPAN 3
16 14 Élie KOPTER 0
17 15 Nordine ATEUR 3
18 16 Youri LIGOTMI 5
19 17 Robin DIDON 5
20 18 Kader HUSSELLE 2
21 19 Paul AUCHON 3
22 20 Ali ABALDAKIN 4
23 21 Annie CROCHE 1
24 22 Fabien GAFATOY 3
25 23 Lydie AUDUVILAGE 5
26 24 Maud TETTE 1
27 25 Medhi KAMAN 2
28 26 Sarah VIGOTTE 4
29 27 Walid TONTIKAY 5
30 28 Yann NIVERSERT 1
31 29 Youssouf DANLBALON 3
32 30 Aude JAVEL 0
33 31 Emma KARENA 0
34 32 Gilbert MUDA 1
35 33 Thibault LONIAISE 5
36 34 Ève ANOUI 5
37 35 Mélanie SANDANLGARAJE 5

View File

@ -0,0 +1,37 @@
"ID";"NAME";"CHAT LEVEL"
0;"Jean NEYMAR";1
1;"Sam DÉPASSE";0
2;"Elsa DORSA";1
3;"Sophie STICKÉ";1
4;"Terry GOLO";0
5;"Marie HONETTE";1
6;"Jérémy TOUTENPLACE";1
7;"Yvon PARTIR";1
8;"Anne HANNA";1
9;"Théo KAY";1
10;"Marin DODOUCE";0
11;"Pierre KIROULE";0
12;"Agathe ZEUBLOUZE";0
13;"Ahmed AYPAN";1
14;"Élie KOPTER";0
15;"Nordine ATEUR";1
16;"Youri LIGOTMI";0
17;"Robin DIDON";0
18;"Kader HUSSELLE";0
19;"Paul AUCHON";0
20;"Ali ABALDAKIN";0
21;"Annie CROCHE";1
22;"Fabien GAFATOY";1
23;"Lydie AUDUVILAGE";1
24;"Maud TETTE";1
25;"Medhi KAMAN";1
26;"Sarah VIGOTTE";1
27;"Walid TONTIKAY";3
28;"Yann NIVERSERT";1
29;"Youssouf DANLBALON";3
30;"Aude JAVEL";3
31;"Emma KARENA";3
32;"Gilbert MUDA";4
33;"Thibault LONIAISE";4
34;"Ève ANOUI";5
35;"Mélanie SANDANLGARAJE";5
1 ID NAME CHAT LEVEL
2 0 Jean NEYMAR 1
3 1 Sam DÉPASSE 0
4 2 Elsa DORSA 1
5 3 Sophie STICKÉ 1
6 4 Terry GOLO 0
7 5 Marie HONETTE 1
8 6 Jérémy TOUTENPLACE 1
9 7 Yvon PARTIR 1
10 8 Anne HANNA 1
11 9 Théo KAY 1
12 10 Marin DODOUCE 0
13 11 Pierre KIROULE 0
14 12 Agathe ZEUBLOUZE 0
15 13 Ahmed AYPAN 1
16 14 Élie KOPTER 0
17 15 Nordine ATEUR 1
18 16 Youri LIGOTMI 0
19 17 Robin DIDON 0
20 18 Kader HUSSELLE 0
21 19 Paul AUCHON 0
22 20 Ali ABALDAKIN 0
23 21 Annie CROCHE 1
24 22 Fabien GAFATOY 1
25 23 Lydie AUDUVILAGE 1
26 24 Maud TETTE 1
27 25 Medhi KAMAN 1
28 26 Sarah VIGOTTE 1
29 27 Walid TONTIKAY 3
30 28 Yann NIVERSERT 1
31 29 Youssouf DANLBALON 3
32 30 Aude JAVEL 3
33 31 Emma KARENA 3
34 32 Gilbert MUDA 4
35 33 Thibault LONIAISE 4
36 34 Ève ANOUI 5
37 35 Mélanie SANDANLGARAJE 5

6
tests/user_table_5.csv Normal file
View File

@ -0,0 +1,6 @@
"ID";"NAME";"CHAT LEVEL"
0;"Jean NEYMAR";1
1;"Sam DÉPASSE";0
2;"Elsa DORSA";2
3;"Sophie STICKÉ";3
4;"Terry GOLO";4
1 ID NAME CHAT LEVEL
2 0 Jean NEYMAR 1
3 1 Sam DÉPASSE 0
4 2 Elsa DORSA 2
5 3 Sophie STICKÉ 3
6 4 Terry GOLO 4

10
tests/user_table_9.csv Normal file
View File

@ -0,0 +1,10 @@
"ID";"NAME";"CHAT LEVEL"
0;"Jean NEYMAR";1
1;"Sam DÉPASSE";0
2;"Elsa DORSA";2
3;"Sophie STICKÉ";3
4;"Terry GOLO";4
5;"Marie HONETTE";1
6;"Jérémy TOUTENPLACE";3
7;"Yvon PARTIR";5
8;"Anne HANNA";2
1 ID NAME CHAT LEVEL
2 0 Jean NEYMAR 1
3 1 Sam DÉPASSE 0
4 2 Elsa DORSA 2
5 3 Sophie STICKÉ 3
6 4 Terry GOLO 4
7 5 Marie HONETTE 1
8 6 Jérémy TOUTENPLACE 3
9 7 Yvon PARTIR 5
10 8 Anne HANNA 2