From 58245c13457167ae57c87774d181c625d10a7fac Mon Sep 17 00:00:00 2001 From: Jeff LANCE Date: Mon, 25 Mar 2019 09:19:04 +0100 Subject: [PATCH] Initial commit. --- Pipfile | 17 ++ Pipfile.lock | 138 +++++++++ docs/Makefile | 20 ++ docs/_static/figure1 | 6 + docs/_static/figure1.png | Bin 0 -> 4024 bytes docs/_static/figure2 | 3 + docs/_static/figure2.png | Bin 0 -> 2340 bytes docs/api.rst | 9 + docs/api/engine.rst | 8 + docs/api/interface.rst | 8 + docs/api/seatingplan.rst | 8 + docs/api/student.rst | 8 + docs/conf.py | 194 ++++++++++++ docs/getting-started.rst | 3 + docs/index.rst | 11 + docs/intro.rst | 483 ++++++++++++++++++++++++++++++ docs/references.rst | 9 + docs/references/bibliography.rst | 17 ++ docs/references/webography.rst | 6 + main.py | 314 ++++++++++++++++++++ requirements.txt | 5 + sage/__init__.py | 9 + sage/engine.py | 383 ++++++++++++++++++++++++ sage/interface.py | 489 +++++++++++++++++++++++++++++++ sage/seatingplan.py | 205 +++++++++++++ sage/student.py | 34 +++ tests/test_empty.csv | 1 + tests/test_fullempty.csv | 0 tests/test_malformed_1.csv | 2 + tests/user_relationships.csv | 8 + tests/user_table_12.csv | 13 + tests/user_table_15.csv | 16 + tests/user_table_16.csv | 17 ++ tests/user_table_36.csv | 37 +++ tests/user_table_36_soft.csv | 37 +++ tests/user_table_5.csv | 6 + tests/user_table_9.csv | 10 + 37 files changed, 2534 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 docs/Makefile create mode 100644 docs/_static/figure1 create mode 100644 docs/_static/figure1.png create mode 100644 docs/_static/figure2 create mode 100644 docs/_static/figure2.png create mode 100644 docs/api.rst create mode 100644 docs/api/engine.rst create mode 100644 docs/api/interface.rst create mode 100644 docs/api/seatingplan.rst create mode 100644 docs/api/student.rst create mode 100644 docs/conf.py create mode 100644 docs/getting-started.rst create mode 100644 docs/index.rst create mode 100644 docs/intro.rst create mode 100644 docs/references.rst create mode 100644 docs/references/bibliography.rst create mode 100644 docs/references/webography.rst create mode 100755 main.py create mode 100644 requirements.txt create mode 100644 sage/__init__.py create mode 100644 sage/engine.py create mode 100644 sage/interface.py create mode 100644 sage/seatingplan.py create mode 100644 sage/student.py create mode 100644 tests/test_empty.csv create mode 100644 tests/test_fullempty.csv create mode 100644 tests/test_malformed_1.csv create mode 100644 tests/user_relationships.csv create mode 100644 tests/user_table_12.csv create mode 100644 tests/user_table_15.csv create mode 100644 tests/user_table_16.csv create mode 100644 tests/user_table_36.csv create mode 100644 tests/user_table_36_soft.csv create mode 100644 tests/user_table_5.csv create mode 100644 tests/user_table_9.csv diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..ad29a56 --- /dev/null +++ b/Pipfile @@ -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" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..5fc48cc --- /dev/null +++ b/Pipfile.lock @@ -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": {} +} diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..b3a6a47 --- /dev/null +++ b/docs/Makefile @@ -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) diff --git a/docs/_static/figure1 b/docs/_static/figure1 new file mode 100644 index 0000000..55bc3d2 --- /dev/null +++ b/docs/_static/figure1 @@ -0,0 +1,6 @@ + ++-------------+-----------+-----------+-----------+-----------+-----------+ +| 0 | 1 | 2 | 3 | 4 | 5 | ++-------------+-----------+-----------+-----------+-----------+-----------+ +| Muet ou très peu bavard | | | Très bavard | ++-------------+-----------+-----------+-----------+-----------+-----------+ diff --git a/docs/_static/figure1.png b/docs/_static/figure1.png new file mode 100644 index 0000000000000000000000000000000000000000..463060e9af665fce1bd4c3cc6d9d0c9e49bbf50c GIT binary patch literal 4024 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 O`OHmGCZ)#bZ~hxqU|h5S literal 0 HcmV?d00001 diff --git a/docs/_static/figure2 b/docs/_static/figure2 new file mode 100644 index 0000000..d53bda9 --- /dev/null +++ b/docs/_static/figure2 @@ -0,0 +1,3 @@ ++----+-----+---------------------+ +| ID | NOM | NIVEAU DE BAVARDAGE | ++----+-----+---------------------+ diff --git a/docs/_static/figure2.png b/docs/_static/figure2.png new file mode 100644 index 0000000000000000000000000000000000000000..1948aa5a05ac9597101d0ba510e1e60f418d3e58 GIT binary patch literal 2340 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 literal 0 HcmV?d00001 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..417405d --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,9 @@ +*** +API +*** + +.. toctree:: + :maxdepth: 1 + :glob: + + api/* diff --git a/docs/api/engine.rst b/docs/api/engine.rst new file mode 100644 index 0000000..388da34 --- /dev/null +++ b/docs/api/engine.rst @@ -0,0 +1,8 @@ +.. _engine: + +======== +Engine +======== + +.. autoclass:: sage.Engine + :members: diff --git a/docs/api/interface.rst b/docs/api/interface.rst new file mode 100644 index 0000000..0d1e151 --- /dev/null +++ b/docs/api/interface.rst @@ -0,0 +1,8 @@ +.. _interface: + +========= +Interface +========= + +.. automodule:: sage.interface + :members: diff --git a/docs/api/seatingplan.rst b/docs/api/seatingplan.rst new file mode 100644 index 0000000..43335b4 --- /dev/null +++ b/docs/api/seatingplan.rst @@ -0,0 +1,8 @@ +.. _seatingplan: + +=========== +SeatingPlan +=========== + +.. autoclass:: sage.SeatingPlan + :members: diff --git a/docs/api/student.rst b/docs/api/student.rst new file mode 100644 index 0000000..87ff04e --- /dev/null +++ b/docs/api/student.rst @@ -0,0 +1,8 @@ +.. _student: + +======== +Student +======== + +.. autoclass:: sage.Student + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..5c33393 --- /dev/null +++ b/docs/conf.py @@ -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 diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 0000000..eb0e0d5 --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,3 @@ +************** +Pour commencer +************** diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..660d0da --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,11 @@ +##################### +SAGE's documentation! +##################### + +.. toctree:: + :maxdepth: 1 + + intro + getting-started + api + references diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 0000000..1a4da67 --- /dev/null +++ b/docs/intro.rst @@ -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`_ diff --git a/docs/references.rst b/docs/references.rst new file mode 100644 index 0000000..58a0f65 --- /dev/null +++ b/docs/references.rst @@ -0,0 +1,9 @@ +********** +Références +********** + +.. toctree:: + :maxdepth: 1 + :glob: + + references/bibliography diff --git a/docs/references/bibliography.rst b/docs/references/bibliography.rst new file mode 100644 index 0000000..b17de6e --- /dev/null +++ b/docs/references/bibliography.rst @@ -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 diff --git a/docs/references/webography.rst b/docs/references/webography.rst new file mode 100644 index 0000000..653a335 --- /dev/null +++ b/docs/references/webography.rst @@ -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 diff --git a/main.py b/main.py new file mode 100755 index 0000000..1862330 --- /dev/null +++ b/main.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Name: main.py +Author: Jeff LANCE +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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..74da886 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +colorclass==2.2.0 +numpy==1.14.3 +pandas==0.22.0 +six==1.11.0 +terminaltables==3.1.0 diff --git a/sage/__init__.py b/sage/__init__.py new file mode 100644 index 0000000..c4f4699 --- /dev/null +++ b/sage/__init__.py @@ -0,0 +1,9 @@ +""" +Name: __init__.py +Author: Jeff LANCE +Date: 29/03/2018 +""" + +from .student import Student +from .seatingplan import SeatingPlan +from .engine import Engine diff --git a/sage/engine.py b/sage/engine.py new file mode 100644 index 0000000..ef08a57 --- /dev/null +++ b/sage/engine.py @@ -0,0 +1,383 @@ +""" +Name: engine.py +Author: Jeff LANCE +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 diff --git a/sage/interface.py b/sage/interface.py new file mode 100644 index 0000000..56df391 --- /dev/null +++ b/sage/interface.py @@ -0,0 +1,489 @@ +""" +Name: interface.py +Author: Jeff LANCE +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') diff --git a/sage/seatingplan.py b/sage/seatingplan.py new file mode 100644 index 0000000..86358bc --- /dev/null +++ b/sage/seatingplan.py @@ -0,0 +1,205 @@ +""" +Name: seatingplan.py +Author: Jeff LANCE +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 diff --git a/sage/student.py b/sage/student.py new file mode 100644 index 0000000..38ed898 --- /dev/null +++ b/sage/student.py @@ -0,0 +1,34 @@ +""" +Name: student.py +Author: Jeff LANCE +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 diff --git a/tests/test_empty.csv b/tests/test_empty.csv new file mode 100644 index 0000000..2516212 --- /dev/null +++ b/tests/test_empty.csv @@ -0,0 +1 @@ +"Id";"Name";"Chat level" diff --git a/tests/test_fullempty.csv b/tests/test_fullempty.csv new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_malformed_1.csv b/tests/test_malformed_1.csv new file mode 100644 index 0000000..cd648d9 --- /dev/null +++ b/tests/test_malformed_1.csv @@ -0,0 +1,2 @@ +"Id";"Name" +0;"Jean AIMAR";4 diff --git a/tests/user_relationships.csv b/tests/user_relationships.csv new file mode 100644 index 0000000..760a328 --- /dev/null +++ b/tests/user_relationships.csv @@ -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 diff --git a/tests/user_table_12.csv b/tests/user_table_12.csv new file mode 100644 index 0000000..49ea701 --- /dev/null +++ b/tests/user_table_12.csv @@ -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 diff --git a/tests/user_table_15.csv b/tests/user_table_15.csv new file mode 100644 index 0000000..0eca72e --- /dev/null +++ b/tests/user_table_15.csv @@ -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 diff --git a/tests/user_table_16.csv b/tests/user_table_16.csv new file mode 100644 index 0000000..699f015 --- /dev/null +++ b/tests/user_table_16.csv @@ -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 diff --git a/tests/user_table_36.csv b/tests/user_table_36.csv new file mode 100644 index 0000000..1b6dd0d --- /dev/null +++ b/tests/user_table_36.csv @@ -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 diff --git a/tests/user_table_36_soft.csv b/tests/user_table_36_soft.csv new file mode 100644 index 0000000..2688d24 --- /dev/null +++ b/tests/user_table_36_soft.csv @@ -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 diff --git a/tests/user_table_5.csv b/tests/user_table_5.csv new file mode 100644 index 0000000..4386f84 --- /dev/null +++ b/tests/user_table_5.csv @@ -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 diff --git a/tests/user_table_9.csv b/tests/user_table_9.csv new file mode 100644 index 0000000..148861d --- /dev/null +++ b/tests/user_table_9.csv @@ -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