From c3789e6ba83a9c9ad7aa9022dd89c9a7e9f062ed Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 24 Apr 2020 02:35:11 +0200 Subject: [PATCH 01/53] chore(git): ignore more files, such as OS or IDE specific files --- .gitignore | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 09271d1..0dae912 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ *.mo .#* -backup - # Python __pycache__/ .mypy_cache @@ -14,9 +12,15 @@ __pycache__/ # Local (secret) configuration /.env.local +# Backup +backup + # Databases (dumps?) test.db main.db +# Operating System +.directory + # IDE /.idea/ From bcf575a26dff850a1def845ff283fa9412bac71f Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 24 Apr 2020 03:40:44 +0200 Subject: [PATCH 02/53] chore(accessibility): add extra spacing for readability to the README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 0b5f8f2..fe5049c 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,16 @@ You certainly want to apply the database migrations with: ./docker/migrate.sh + ## Run the tests ./docker/test.sh +## Create databases migrations + + sudo ./docker/makemigrations.sh + ## Local development From 1b2ff5adf3c2b9f6f2075bd12681106948843fc0 Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 24 Apr 2020 06:26:42 +0200 Subject: [PATCH 03/53] style(github): harmonize the headers in the issue template --- .github/ISSUE_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 66b6b03..3f714ad 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,8 +3,8 @@ ### Version -#### Expected +### Expected -#### Actual +### Actual ### Additional Details From be619544b4a8c0f37aed31e85534e9b577999181 Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 24 Apr 2020 11:19:26 +0200 Subject: [PATCH 04/53] chore(PEP8) --- libs/majority_judgment.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 libs/majority_judgment.py diff --git a/libs/majority_judgment.py b/libs/majority_judgment.py new file mode 100644 index 0000000..e69de29 From d071e0a3e5520ef9ba5e507d4237f677433b9ebc Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 24 Apr 2020 11:23:05 +0200 Subject: [PATCH 05/53] chore(pip): add forlorn `dataclasses` to the list of python dependencies --- requirements.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a784978..5f93eb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,8 @@ psycopg2==2.9.5 git+https://github.com/MieuxVoter/majority-judgment-library-python python-jose==3.3.0 python-dateutil==2.8.2 -pydantic-settings==2.9.1 \ No newline at end of file +pydantic-settings==2.9.1 + +# Testing +coverage>=4.5.1 +nose==1.3.7 \ No newline at end of file From 12878116ce351c5d719968ea57d9d9d76c69e94c Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 24 Apr 2020 11:47:54 +0200 Subject: [PATCH 06/53] chore(PEP8) --- mvapi/settings.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 mvapi/settings.py diff --git a/mvapi/settings.py b/mvapi/settings.py new file mode 100644 index 0000000..e69de29 From fcbb7020ccf6422ea5129b506572b1a4f194e5b9 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 25 Apr 2020 01:45:55 +0200 Subject: [PATCH 07/53] chore(PEP8) --- election/models.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 election/models.py diff --git a/election/models.py b/election/models.py new file mode 100644 index 0000000..e69de29 From 8cef79a5a75845f105e45f82411fd94fa1fc1c81 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 25 Apr 2020 06:41:55 +0200 Subject: [PATCH 08/53] chore(consistency)!: use plural form since it is an array BREAKING CHANGE: `LANGUAGE_AVAILABLE` -> `LANGUAGES_AVAILABLE` --- election/views.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 election/views.py diff --git a/election/views.py b/election/views.py new file mode 100644 index 0000000..e69de29 From bf31ace5bff37d4e92ecd3f6a3a0eb77ee2dce45 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 02:09:21 +0200 Subject: [PATCH 09/53] feat(features): Draft the very first scenario. Let's experiment with `behave-django`. The directory `features/fr` ought to be a git submodule. We now need consent from everyone about the first scenario before we can proceed. Keep in mind that we need to write one scenario at a time, then implement it, and then write another. We must not write many scenarios at once. It's going to be tempting. Don't do it. https://www.lesswrong.com/posts/uHYYA32CKgKT3FagE/hold-off-on-proposing-solutions This should be considered like a Constitution, where each article is thoroughly debated and carefully worded. There will be some technical constraints in Gherkin that Constitutions don't have. --- .behaverc | 7 +++ requirements.txt | 2 +- tests/acceptance/environment.py | 51 +++++++++++++++++++ .../features/fr/10.creer-un-scrutin.feature | 43 ++++++++++++++++ tests/acceptance/steps/user_crud.py | 25 +++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 .behaverc create mode 100644 tests/acceptance/environment.py create mode 100644 tests/acceptance/features/fr/10.creer-un-scrutin.feature create mode 100644 tests/acceptance/steps/user_crud.py diff --git a/.behaverc b/.behaverc new file mode 100644 index 0000000..d97122a --- /dev/null +++ b/.behaverc @@ -0,0 +1,7 @@ +[behave] +; perhaps we want some of those? +;format=plain +;logging_clear_handlers=yes +;logging_filter=-suds + +paths=tests/acceptance \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5f93eb6..824f408 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,4 @@ pydantic-settings==2.9.1 # Testing coverage>=4.5.1 -nose==1.3.7 \ No newline at end of file +nose==1.3.7 diff --git a/tests/acceptance/environment.py b/tests/acceptance/environment.py new file mode 100644 index 0000000..96b305d --- /dev/null +++ b/tests/acceptance/environment.py @@ -0,0 +1,51 @@ +""" +Environment module for acceptance testing of the scenaristic constitution. +https://behave.readthedocs.io/en/latest/api.html#environment-file-functions +""" + + +# Since we expect most of our step defs to require the flexibity of regexes, +# we make regular expressions the default. (I18N, epicene) +# This is at the expense of automatic type casting of step variables. +# It's a somewhat good tradeoff since natural language numbers still do require +# explicit casting in all matchers (AFAIK in early 2020). +from behave import use_step_matcher +use_step_matcher("re") +# You can still override this right before your step def if you want, +# by calling use_step_matcher() with one of the following: +# "parse" (factory default), "cfparse" +# and calling it again with "re" after your step def. (it uses a `global`) + + +# def _log(*args): +# print(*args) # hook to logger instead + + +def before_feature(context, feature): + """ + Ran before _each_ feature file is exercised. + """ + # _log("Before feature", feature) + pass + # context.fixtures = ['behave-fixtures.json'] + + +def before_scenario(context, scenario): + """ + Ran before _each_ scenario is run. + """ + # _log("Before scenario", scenario) + pass + # context.fixtures.append('behave-second-fixture.json') + + +def before_all(context): + """ + Ran before the whole shooting match. + """ + pass + + +def django_ready(context): + # _log("Django Ready", context) + context.django = True diff --git a/tests/acceptance/features/fr/10.creer-un-scrutin.feature b/tests/acceptance/features/fr/10.creer-un-scrutin.feature new file mode 100644 index 0000000..85cd1ed --- /dev/null +++ b/tests/acceptance/features/fr/10.creer-un-scrutin.feature @@ -0,0 +1,43 @@ +#language: fr +@fr +Fonctionnalité: Créer un scrutin au jugement majoritaire sur app.mieuxvoter.fr + Dans le but de décider collectivement + En tant que collectif démocratique moderne + Nous souhaitons créer un scrutin au jugement majoritaire + + # Écrivez d'autres intentions, si vous le souhaitez +# Dans le but de ? +# En tant que ? +# Nous|Je ? + + +Contexte: + Étant donné un citoyen nommé Michel Balinski + Et un citoyen nommé Rida Laraki + Et une citoyenne nommée Maria Balinska + # … + + + +Scénario: Créer un scrutin au jugement majoritaire + Quand ??? créé un scrutin comme suit: + """ + titre: ??? + candidats: + - ??? + - ??? + - ??? + ???: ??? + """ + Et ??? vote comme suit sur ce scrutin: + """ + ???: ??? + ???: ??? + ???: ??? + """ + Et … + Alors ??? doit être le candidat élu de ce scrutin + # … + # @all: Avez-vous des exemples de scrutins ? + # N'hésitez pas à vous aussi écrire ce scénario dans votre branche, + # si besoin on pourra délibérer au JM dessus ;) diff --git a/tests/acceptance/steps/user_crud.py b/tests/acceptance/steps/user_crud.py new file mode 100644 index 0000000..53d129d --- /dev/null +++ b/tests/acceptance/steps/user_crud.py @@ -0,0 +1,25 @@ +############################################################################### +# Is PyCharm > 2017.3 understanding these for you? +# from behave import given, when, then +# 2017.3 won't, and it's all I got. +# Meanwhile, let's use `step' for the sprint. +from behave import step +# FIXME: swap to usage of `given`, `when`, `then` when relevant +############################################################################### + +# Alternatives: move this to __init__.py ? +# from behave import use_step_matcher +# use_step_matcher("re") +# Aliases? +# ⋅e = (?:[⋅.-]?e|) +# ⋅ne = (?:[⋅.-]?ne|) + + +# + +# @given +@step(u"un(?:[⋅.-]?e|) citoyen(?:[⋅.-]?ne|) nommé(?:[⋅.-]?e|) (?P.+)") +def create_citizen_named(context, name): + print("Creating citizen named `%s'…" % name) + raise NotImplementedError() + From 0088142855df69fb8e7f2f0bafba8c27f98ba31b Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 02:50:26 +0200 Subject: [PATCH 10/53] =?UTF-8?q?feat(features):=20add=20a=20step=20doing?= =?UTF-8?q?=20nothing=20for=20`etc.`,=20`=E2=80=A6`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/acceptance/steps/poetry.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/acceptance/steps/poetry.py diff --git a/tests/acceptance/steps/poetry.py b/tests/acceptance/steps/poetry.py new file mode 100644 index 0000000..fd240a2 --- /dev/null +++ b/tests/acceptance/steps/poetry.py @@ -0,0 +1,12 @@ +""" +These steps should effect nothing. +They help keep the features idiomatic, suggest additions, etc. +""" + +from behave import step + + +@step(u"etc[.]?|…|[.]{2,}") +def et_caetera(context): + pass + From 1947ba92290ac02edfb5ce5c1060ed5ad61d1b26 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 06:45:53 +0200 Subject: [PATCH 11/53] =?UTF-8?q?feat(features):=20Requ=C3=A9rir=20la=20pr?= =?UTF-8?q?=C3=A9sence=20=C3=A0=20priori=20de=20citoyen=E2=8B=85nes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a meta feature about the steps in the features. This way I can work on stuff without deciding on the scenarios. --- .../features/fr/00.deboguer-les-etapes.feature | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/acceptance/features/fr/00.deboguer-les-etapes.feature diff --git a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature new file mode 100644 index 0000000..4f09631 --- /dev/null +++ b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature @@ -0,0 +1,17 @@ +#language: fr +@fr +Fonctionnalité: Décrire les comportements des étapes des scénarios + Dans le but de … + En tant que … + Nous souhaitons … + + +Scénario: Requérir la présence à priori de citoyen⋅nes + # New keyword Sachant has not yet propagated to our behave version + Sachant qu'il ne devrait y avoir aucun citoyen dans la base de données + # So we use the less idiomatic equivalent + Étant donné qu'il ne devrait y avoir aucun citoyen dans la base de données + Étant donné un citoyen nommé Michel Balinski + Alors il devrait y avoir un citoyen dans la base de données + + From 14c2ff49da61f59d258d44361d357786bffad6d6 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 07:11:42 +0200 Subject: [PATCH 12/53] feat(features): implement the first two user-related steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note how naive parse_amount() is for now. … We cheated. --- requirements.txt | 2 + .../steps/{poetry.py => steps_poetry.py} | 0 tests/acceptance/steps/steps_user_crud.py | 45 +++++++++++++++++++ tests/acceptance/steps/tools_db.py | 9 ++++ tests/acceptance/steps/tools_nlp.py | 19 ++++++++ tests/acceptance/steps/user_crud.py | 25 ----------- 6 files changed, 75 insertions(+), 25 deletions(-) rename tests/acceptance/steps/{poetry.py => steps_poetry.py} (100%) create mode 100644 tests/acceptance/steps/steps_user_crud.py create mode 100644 tests/acceptance/steps/tools_db.py create mode 100644 tests/acceptance/steps/tools_nlp.py delete mode 100644 tests/acceptance/steps/user_crud.py diff --git a/requirements.txt b/requirements.txt index 824f408..b8dabfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,5 @@ pydantic-settings==2.9.1 # Testing coverage>=4.5.1 nose==1.3.7 +#behave-django==1.3.0 +word2number==1.1 diff --git a/tests/acceptance/steps/poetry.py b/tests/acceptance/steps/steps_poetry.py similarity index 100% rename from tests/acceptance/steps/poetry.py rename to tests/acceptance/steps/steps_poetry.py diff --git a/tests/acceptance/steps/steps_user_crud.py b/tests/acceptance/steps/steps_user_crud.py new file mode 100644 index 0000000..f1e7543 --- /dev/null +++ b/tests/acceptance/steps/steps_user_crud.py @@ -0,0 +1,45 @@ +############################################################################### +# from behave import given, when, then +# Pycharm 2020.1 won't understand the above, but it understands `step`. +from behave import step +# Swap to usage of `given`, `when`, `then` when relevant +############################################################################### + + +from tools_nlp import parse_amount +from tools_db import count_users + + +############################################################################### + +# Aliases? +# ⋅e = (?:[⋅.-]?e|) +# ⋅ne = (?:[⋅.-]?ne|) +# ⋅nes = (?:[⋅.-]?ne|)s? + +############################################################################### + + +# @given +@step(u"un(?:[⋅.-]?e|) citoyen(?:[⋅.-]?ne|) nommé(?:[⋅.-]?e|) (?P.+)") +def create_citizen_named(context, name): + print("Creating citizen named `%s'…" % name) + from django.contrib.auth.models import User + user = User.objects.create_user( + username=name, + email='user@test.mieuxvoter.fr', + password=name + ) + + +# @then +@step(u"(?:qu')?il ne devrait y avoir aucun citoyen dans la base de données") +def there_should_not_be_any_user(context): + assert(count_users() == 0) + + +# @then +@step(u"(?:qu')?il devrait y avoir (?P.+) citoyen(?:[⋅.-]?ne?|)s? dans la base de données") +def there_should_be_n_users(context, amount): + amount = parse_amount(context, amount) + assert(count_users() == amount) diff --git a/tests/acceptance/steps/tools_db.py b/tests/acceptance/steps/tools_db.py new file mode 100644 index 0000000..cd4812b --- /dev/null +++ b/tests/acceptance/steps/tools_db.py @@ -0,0 +1,9 @@ +""" +Local database abstraction layer for step defs. +""" + +from django.contrib.auth.models import User + + +def count_users(): + return User.objects.count() diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py new file mode 100644 index 0000000..390dd97 --- /dev/null +++ b/tests/acceptance/steps/tools_nlp.py @@ -0,0 +1,19 @@ + +# Natural language processing tools + + +def parse_amount(context, amount_string): + + # TODO: find the lib(s) implementing what we want here + # - num2words odes it the other way around + # - word2number looks ok, but for english only + # - spacy ? or our very own tensorflow experiment + + if 'fr' in context.tags: + # :(|) + if 'un' == amount_string: + return 1 + raise NotImplemented() + + from word2number import w2n + return w2n.word_to_num(amount_string) diff --git a/tests/acceptance/steps/user_crud.py b/tests/acceptance/steps/user_crud.py deleted file mode 100644 index 53d129d..0000000 --- a/tests/acceptance/steps/user_crud.py +++ /dev/null @@ -1,25 +0,0 @@ -############################################################################### -# Is PyCharm > 2017.3 understanding these for you? -# from behave import given, when, then -# 2017.3 won't, and it's all I got. -# Meanwhile, let's use `step' for the sprint. -from behave import step -# FIXME: swap to usage of `given`, `when`, `then` when relevant -############################################################################### - -# Alternatives: move this to __init__.py ? -# from behave import use_step_matcher -# use_step_matcher("re") -# Aliases? -# ⋅e = (?:[⋅.-]?e|) -# ⋅ne = (?:[⋅.-]?ne|) - - -# - -# @given -@step(u"un(?:[⋅.-]?e|) citoyen(?:[⋅.-]?ne|) nommé(?:[⋅.-]?e|) (?P.+)") -def create_citizen_named(context, name): - print("Creating citizen named `%s'…" % name) - raise NotImplementedError() - From d67bdeee8c5df3c44e579c8499c9a16083da91c9 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 07:19:48 +0200 Subject: [PATCH 13/53] docs(features): document installation without Docker --- INSTALL.md | 68 +++++++++++++++++++ tests/acceptance/README.md | 4 ++ .../features/fr/10.creer-un-scrutin.feature | 2 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 INSTALL.md create mode 100644 tests/acceptance/README.md diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..c549a7f --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,68 @@ + +## Install using Docker + +> see root README + +--- + +## Install using virtualenv + +> It's usually less painful to install using Docker instead. + + +### Install the required packages + + +#### Ubuntu & Debian + + + sudo apt install python3 python3-dev virtualenv + sudo apt install postgresql-server-dev-all + + +### Set up a virtualenv + + virtualenv .venv --python=python3 + source .venv/bin/activate + + +### Install Python dependencies + + pip install -r requirements.txt + + +### Configure your instance + + cp .env .env.local + nano .env.local + … + source .env.local + + +### Bootstrap PostgreSQL Database + + sudo -u postgres psql + postgres=# ALTER USER postgres PASSWORD 'MyNotSoSecretPassword'; + postgres=# CREATE DATABASE mvapi; + python manage.py migrate + + +### Create an Admin user + + python manage.py createsuperuser + + +### Develop + +To run each time you're setting up your shell. + + source .venv/bin/activate + source env.local + python manage.py runserver + +Visit http://localhost:8000/admin + + +### Test + + python manage.py behave diff --git a/tests/acceptance/README.md b/tests/acceptance/README.md new file mode 100644 index 0000000..f4fa23d --- /dev/null +++ b/tests/acceptance/README.md @@ -0,0 +1,4 @@ + +Run all scenarios except the ones tagged with `@wip`: + + python manage.py behave --no-skipped --tags=~@wip diff --git a/tests/acceptance/features/fr/10.creer-un-scrutin.feature b/tests/acceptance/features/fr/10.creer-un-scrutin.feature index 85cd1ed..a93e175 100644 --- a/tests/acceptance/features/fr/10.creer-un-scrutin.feature +++ b/tests/acceptance/features/fr/10.creer-un-scrutin.feature @@ -18,7 +18,7 @@ Contexte: # … - +@wip Scénario: Créer un scrutin au jugement majoritaire Quand ??? créé un scrutin comme suit: """ From fc63bdcd802b812f62f777aeffe62d7fd6bc0117 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 14:18:03 +0200 Subject: [PATCH 14/53] feat(features): Require polls as given MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Requérir la présence à priori de scrutins." Another brick in setting up the contexts we'll want for our scenarios. Language detection is a dummy for now. But we're add a rather fault-intolerant but working YAML perser to use with step pystrings. I18N support is overall made of tape (not even cardboard). --- requirements.txt | 1 + .../fr/00.deboguer-les-etapes.feature | 16 ++++++- tests/acceptance/steps/steps_scrutin_crud.py | 38 +++++++++++++++ tests/acceptance/steps/tools_db.py | 8 ++++ tests/acceptance/steps/tools_i18n.py | 8 ++++ tests/acceptance/steps/tools_nlp.py | 46 ++++++++++++++++++- 6 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 tests/acceptance/steps/steps_scrutin_crud.py create mode 100644 tests/acceptance/steps/tools_i18n.py diff --git a/requirements.txt b/requirements.txt index b8dabfc..c1f3d75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ coverage>=4.5.1 nose==1.3.7 #behave-django==1.3.0 word2number==1.1 +PyYAML==5.3.1 diff --git a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature index 4f09631..00cefb4 100644 --- a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature @@ -8,10 +8,24 @@ Fonctionnalité: Décrire les comportements des étapes des scénarios Scénario: Requérir la présence à priori de citoyen⋅nes # New keyword Sachant has not yet propagated to our behave version - Sachant qu'il ne devrait y avoir aucun citoyen dans la base de données + #Sachant qu'il ne devrait y avoir aucun citoyen dans la base de données # So we use the less idiomatic equivalent Étant donné qu'il ne devrait y avoir aucun citoyen dans la base de données Étant donné un citoyen nommé Michel Balinski Alors il devrait y avoir un citoyen dans la base de données +Scénario: Requérir la présence à priori de scrutins + Étant donné qu'il ne devrait y avoir aucun scrutin dans la base de données + Étant donné un scrutin comme suit: + """ + titre: Responsable de l'animation du chantier Constituance Algorithmique + candidats: + - Pierre-Louis Guhur + - Chloé Ridel + - Dominique Merle + """ + Alors il devrait y avoir un scrutin dans la base de données + + +# Scénario: Soumettre un nouveau scrutin diff --git a/tests/acceptance/steps/steps_scrutin_crud.py b/tests/acceptance/steps/steps_scrutin_crud.py new file mode 100644 index 0000000..f446aaa --- /dev/null +++ b/tests/acceptance/steps/steps_scrutin_crud.py @@ -0,0 +1,38 @@ +############################################################################### +# from behave import given, when, then +# Pycharm 2020.1 won't understand the above, but it understands `step`. +from behave import step +from yaml import safe_load +# Swap to usage of `given`, `when`, `then` when relevant +############################################################################### + + +from tools_nlp import parse_amount, parse_yaml +from tools_db import count_polls + + +############################################################################### + +# @given +@step(u"un scrutin comme suit:?") +def there_is_a_poll_like_so(context): + data = parse_yaml(context) + from election.models import Election + election = Election() + election.title = data['title'] + election.candidates = data['candidates'] + election.num_grades = 7 + election.save() + + +# @then +@step(u"(?:qu')?il ne devrait y avoir aucun scrutin dans la base de données") +def there_should_not_be_any_poll(context): + assert(count_polls() == 0) + + +# @then +@step(u"(?:qu')?il devrait y avoir (?P.+) scrutins? dans la base de données") +def there_should_be_n_polls(context, amount): + amount = parse_amount(context, amount) + assert(count_polls() == amount) diff --git a/tests/acceptance/steps/tools_db.py b/tests/acceptance/steps/tools_db.py index cd4812b..d453bda 100644 --- a/tests/acceptance/steps/tools_db.py +++ b/tests/acceptance/steps/tools_db.py @@ -2,8 +2,16 @@ Local database abstraction layer for step defs. """ + from django.contrib.auth.models import User +from election.models import Election def count_users(): return User.objects.count() + + +def count_polls(): # TBD: "scrutin" translates to "poll"? + return Election.objects.count() + + diff --git a/tests/acceptance/steps/tools_i18n.py b/tests/acceptance/steps/tools_i18n.py new file mode 100644 index 0000000..2574c48 --- /dev/null +++ b/tests/acceptance/steps/tools_i18n.py @@ -0,0 +1,8 @@ +""" +Internationalization tools for step definitions. +""" + + +def guess_language(context): + return 'fr' # FIXME use context.tags + diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index 390dd97..1bad347 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -1,8 +1,19 @@ -# Natural language processing tools +# Natural Language Processing tools + + +from tools_i18n import guess_language def parse_amount(context, amount_string): + """ + - Probably Not Invented Here ; help us find where it's at + - Multilingual (hopefully) EN - FR - … + - Tailored for Gherkin features and behave + :param context: Context object from behave step def + :param string amount_string: + :return int|float: + """ # TODO: find the lib(s) implementing what we want here # - num2words odes it the other way around @@ -10,10 +21,41 @@ def parse_amount(context, amount_string): # - spacy ? or our very own tensorflow experiment if 'fr' in context.tags: - # :(|) + if amount_string.startswith('aucun'): + return 0 if 'un' == amount_string: return 1 + # :(|) raise NotImplemented() from word2number import w2n return w2n.word_to_num(amount_string) + + +def parse_yaml(context, with_i18n=True): + """ + :param context: Context object from behave step def + :param bool with_i18n: + :return: The step pystring contents as dictionary from YAML + """ + from yaml import safe_load + data = safe_load(context.text) + + if with_i18n: + language = guess_language(context) + # Eventually, load these maps from files, perhaps + yaml_keys_map = { + 'fr': { + 'titre': 'title', + 'candidats': 'candidates', + 'candidates': 'candidates', + 'candidat⋅es': 'candidates', + }, + } + # Naive mapping, not collision-resilient, but it's ok for now + if language in yaml_keys_map: + for key in [k for k in data.keys()]: + if key in yaml_keys_map[language]: + data[yaml_keys_map[language][key]] = data[key] + + return data From 22b81beb082f5133e25ea7c76fc0d852c48d849f Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 14:18:47 +0200 Subject: [PATCH 15/53] chore(PEP8) --- tests/acceptance/steps/steps_user_crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/steps/steps_user_crud.py b/tests/acceptance/steps/steps_user_crud.py index f1e7543..66d3b17 100644 --- a/tests/acceptance/steps/steps_user_crud.py +++ b/tests/acceptance/steps/steps_user_crud.py @@ -25,7 +25,7 @@ def create_citizen_named(context, name): print("Creating citizen named `%s'…" % name) from django.contrib.auth.models import User - user = User.objects.create_user( + _user = User.objects.create_user( username=name, email='user@test.mieuxvoter.fr', password=name From a40a427dbed3ac7289a62644d3324e11c7a56662 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 14:27:20 +0200 Subject: [PATCH 16/53] config(features): don't show the clutter --- .behaverc | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.behaverc b/.behaverc index d97122a..5892f9b 100644 --- a/.behaverc +++ b/.behaverc @@ -4,4 +4,18 @@ ;logging_clear_handlers=yes ;logging_filter=-suds -paths=tests/acceptance \ No newline at end of file +; We are not using the default path to leave room for unit tests? +paths=tests/acceptance + +; Show all print() statements even if tests pass. (bugs! bugs!) +;stderr_capture=False +;stdout_capture=False + +; Skipped projects clutter the console when using tags such as : +; $ python manage.py behave --tags=wip +; You can add a tag to a feature by using its name as descriptor : +; @wip +; Feature: Working hard +; [...] +; You can also add tags to Scenarios ! +show_skipped=False \ No newline at end of file From 5cb13a169e46da152e766bd938b7b352d859adb6 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 16:05:53 +0200 Subject: [PATCH 17/53] feat(feature): Count polls --- requirements.txt | 5 +++- .../fr/00.deboguer-les-etapes.feature | 30 ++++++++++++++++--- tests/acceptance/steps/steps_poetry.py | 4 +++ tests/acceptance/steps/steps_scrutin_crud.py | 17 ++++------- tests/acceptance/steps/tools_nlp.py | 29 ++++++++---------- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/requirements.txt b/requirements.txt index c1f3d75..e222b1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,9 @@ pydantic-settings==2.9.1 # Testing coverage>=4.5.1 nose==1.3.7 -#behave-django==1.3.0 +# DEPRECATED ? # word2number==1.1 +################ +text2num==2.2.0 PyYAML==5.3.1 +#behave-django==1.3.0 diff --git a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature index 00cefb4..8a99acf 100644 --- a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature @@ -1,9 +1,9 @@ #language: fr @fr Fonctionnalité: Décrire les comportements des étapes des scénarios - Dans le but de … - En tant que … - Nous souhaitons … + Dans le but d'expliciter le comportement des étapes des scénarios + En tant que débogueur⋅es + Nous souhaitons les utiliser dans différents contextes Scénario: Requérir la présence à priori de citoyen⋅nes @@ -16,7 +16,7 @@ Scénario: Requérir la présence à priori de citoyen⋅nes Scénario: Requérir la présence à priori de scrutins - Étant donné qu'il ne devrait y avoir aucun scrutin dans la base de données + Sachant qu'il ne devrait y avoir aucun scrutin dans la base de données Étant donné un scrutin comme suit: """ titre: Responsable de l'animation du chantier Constituance Algorithmique @@ -28,4 +28,26 @@ Scénario: Requérir la présence à priori de scrutins Alors il devrait y avoir un scrutin dans la base de données +Scénario: Compter les scrutins + Sachant qu'il ne devrait y avoir aucun scrutin dans la base de données + Étant donné un scrutin comme suit: + """ + titre: Mon app JM préférée + candidats: + - app.mieuxvoter.fr + - jugementmajoritaire.net + - lechoixcommun.fr + """ + Alors il devrait y avoir un scrutin dans la base de données + Mais ce n'est pas tout ! + Étant donné un autre scrutin comme suit: + """ + titre: Canaux de communication interne + candidats: + - Telegram + - Telegram + """ + Alors il devrait maintenant y avoir deux scrutins dans la base de données + + # Scénario: Soumettre un nouveau scrutin diff --git a/tests/acceptance/steps/steps_poetry.py b/tests/acceptance/steps/steps_poetry.py index fd240a2..c02b405 100644 --- a/tests/acceptance/steps/steps_poetry.py +++ b/tests/acceptance/steps/steps_poetry.py @@ -10,3 +10,7 @@ def et_caetera(context): pass + +@step(u"ce n'est pas tout *!*:?") +def but_wait_there_is_more(context): + pass diff --git a/tests/acceptance/steps/steps_scrutin_crud.py b/tests/acceptance/steps/steps_scrutin_crud.py index f446aaa..1a44649 100644 --- a/tests/acceptance/steps/steps_scrutin_crud.py +++ b/tests/acceptance/steps/steps_scrutin_crud.py @@ -13,26 +13,21 @@ ############################################################################### + # @given -@step(u"un scrutin comme suit:?") +@step(u"un(?: autre)? scrutin comme suit:?") def there_is_a_poll_like_so(context): data = parse_yaml(context) from election.models import Election election = Election() - election.title = data['title'] - election.candidates = data['candidates'] - election.num_grades = 7 + election.title = data.get('title') + election.candidates = data.get('candidates') + election.num_grades = data.get('grades', 7) election.save() # @then -@step(u"(?:qu')?il ne devrait y avoir aucun scrutin dans la base de données") -def there_should_not_be_any_poll(context): - assert(count_polls() == 0) - - -# @then -@step(u"(?:qu')?il devrait y avoir (?P.+) scrutins? dans la base de données") +@step(u"(?:qu')?il(?: ne)? devrait(?: maintenant)? y avoir (?P.+) scrutins? dans la base de données") def there_should_be_n_polls(context, amount): amount = parse_amount(context, amount) assert(count_polls() == amount) diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index 1bad347..d2415e5 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -1,35 +1,30 @@ +""" +Natural Language Processing tools +""" -# Natural Language Processing tools - +import re +from text_to_num import text2num from tools_i18n import guess_language def parse_amount(context, amount_string): """ - - Probably Not Invented Here ; help us find where it's at - Multilingual (hopefully) EN - FR - … - Tailored for Gherkin features and behave :param context: Context object from behave step def :param string amount_string: :return int|float: """ - - # TODO: find the lib(s) implementing what we want here - # - num2words odes it the other way around - # - word2number looks ok, but for english only - # - spacy ? or our very own tensorflow experiment - - if 'fr' in context.tags: - if amount_string.startswith('aucun'): + language = guess_language(context) + if 'fr' == language: + if re.match("^aucun(?:[⋅.-]?e)?$", amount_string): + return 0 + elif 'en' == language: + if re.match("^no(?:ne)?$", amount_string): return 0 - if 'un' == amount_string: - return 1 - # :(|) - raise NotImplemented() - from word2number import w2n - return w2n.word_to_num(amount_string) + return text2num(text=amount_string, lang=language, relaxed=True) def parse_yaml(context, with_i18n=True): From 0f9c302fb4ea2b095721b81a1e1aff5960af2abe Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 16:10:09 +0200 Subject: [PATCH 18/53] feat(features): use `text2num`, found by @guhur ! --- requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index e222b1a..539f395 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,9 +11,6 @@ pydantic-settings==2.9.1 # Testing coverage>=4.5.1 nose==1.3.7 -# DEPRECATED ? # -word2number==1.1 -################ -text2num==2.2.0 +text2num>=2.2.0 PyYAML==5.3.1 #behave-django==1.3.0 From d882dc060d4fde13df916da56a4b7e1e52e38b04 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 17:27:05 +0200 Subject: [PATCH 19/53] docs(deatures): paint a sign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eval is Evil. – Your friend, _The Sign Painter_ --- tests/acceptance/steps/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/acceptance/steps/__init__.py diff --git a/tests/acceptance/steps/__init__.py b/tests/acceptance/steps/__init__.py new file mode 100644 index 0000000..913d27e --- /dev/null +++ b/tests/acceptance/steps/__init__.py @@ -0,0 +1,4 @@ + +# What can we do from here? +# Not sure whether behave's exec() allows us to hook things in here at all. +# Be warned. HERE BE DRAGONS From a7cc0aeccc10179f3187cf4cd72d4d63709f67be Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 17:37:43 +0200 Subject: [PATCH 20/53] feat(features): add hamcrest as assertion library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tested: - raw assert (not explicit) - describe (stale) - compare (stale) - nose-tools (very few tools, no?) Not tested - behave-pytest (404, did not inquire further) - etc. Looks like full I18N support is not coming out of the box. https://github.com/oboff/hamcrest/issues/80 Inquiring further… --- requirements.txt | 16 +++++++++++++--- tests/acceptance/steps/steps_poetry.py | 4 ++-- tests/acceptance/steps/steps_scrutin_crud.py | 4 +++- tests/acceptance/steps/steps_user_crud.py | 11 +++-------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index 539f395..a1b345c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,19 @@ python-jose==3.3.0 python-dateutil==2.8.2 pydantic-settings==2.9.1 -# Testing -coverage>=4.5.1 +# Test Runners nose==1.3.7 +#django-nose==1.4.6 +#behave-django==1.3.0 + +# Assertion library +pyhamcrest==2.0.2 + +# Test Metrics +coverage>=4.5.1 + +# Natural Language Processor for numbers text2num>=2.2.0 + +# Markup language for humans PyYAML==5.3.1 -#behave-django==1.3.0 diff --git a/tests/acceptance/steps/steps_poetry.py b/tests/acceptance/steps/steps_poetry.py index c02b405..5e2c8ce 100644 --- a/tests/acceptance/steps/steps_poetry.py +++ b/tests/acceptance/steps/steps_poetry.py @@ -1,6 +1,6 @@ """ -These steps should effect nothing. -They help keep the features idiomatic, suggest additions, etc. +These steps should [ae]ffect nothing. +They help keep the features idiomatic, engaging, etc. """ from behave import step diff --git a/tests/acceptance/steps/steps_scrutin_crud.py b/tests/acceptance/steps/steps_scrutin_crud.py index 1a44649..369b59d 100644 --- a/tests/acceptance/steps/steps_scrutin_crud.py +++ b/tests/acceptance/steps/steps_scrutin_crud.py @@ -7,6 +7,8 @@ ############################################################################### +from hamcrest import assert_that, equal_to + from tools_nlp import parse_amount, parse_yaml from tools_db import count_polls @@ -30,4 +32,4 @@ def there_is_a_poll_like_so(context): @step(u"(?:qu')?il(?: ne)? devrait(?: maintenant)? y avoir (?P.+) scrutins? dans la base de données") def there_should_be_n_polls(context, amount): amount = parse_amount(context, amount) - assert(count_polls() == amount) + assert_that(count_polls(), equal_to(amount)) diff --git a/tests/acceptance/steps/steps_user_crud.py b/tests/acceptance/steps/steps_user_crud.py index 66d3b17..b310343 100644 --- a/tests/acceptance/steps/steps_user_crud.py +++ b/tests/acceptance/steps/steps_user_crud.py @@ -5,6 +5,7 @@ # Swap to usage of `given`, `when`, `then` when relevant ############################################################################### +from hamcrest import assert_that, equal_to from tools_nlp import parse_amount from tools_db import count_users @@ -33,13 +34,7 @@ def create_citizen_named(context, name): # @then -@step(u"(?:qu')?il ne devrait y avoir aucun citoyen dans la base de données") -def there_should_not_be_any_user(context): - assert(count_users() == 0) - - -# @then -@step(u"(?:qu')?il devrait y avoir (?P.+) citoyen(?:[⋅.-]?ne?|)s? dans la base de données") +@step(u"(?:qu')?il(?: ne)? devrait y avoir (?P.+) citoyen(?:[⋅.-]?ne?|)s? dans la base de données") def there_should_be_n_users(context, amount): amount = parse_amount(context, amount) - assert(count_users() == amount) + assert_that(count_users(), equal_to(amount)) From 320776eeb98ce794ad609a2ffa76225b61300fe6 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 19:03:23 +0200 Subject: [PATCH 21/53] feat(features): hook some I18N by guessing the language Our language pool is the one of `text2num`. (at least en, fr, es) Gherkin itself is widely translated and extendable in behave IIRC. This commit introduces a huge list of `ISO 639-1` codes. Esperanto is often overlooked in centralized databases. --- tests/acceptance/steps/tools_i18n.py | 211 ++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/tests/acceptance/steps/tools_i18n.py b/tests/acceptance/steps/tools_i18n.py index 2574c48..2603cd0 100644 --- a/tests/acceptance/steps/tools_i18n.py +++ b/tests/acceptance/steps/tools_i18n.py @@ -2,7 +2,216 @@ Internationalization tools for step definitions. """ +languages = [ + # ISO 639-1, Full Name + ('aa', 'Afar'), + ('ab', 'Abkhazian'), + ('af', 'Afrikaans'), + ('ak', 'Akan'), + ('sq', 'Albanian'), + ('am', 'Amharic'), + ('ar', 'Arabic'), + ('an', 'Aragonese'), + ('hy', 'Armenian'), + ('as', 'Assamese'), + ('av', 'Avaric'), + ('ae', 'Avestan'), + ('ay', 'Aymara'), + ('az', 'Azerbaijani'), + ('ba', 'Bashkir'), + ('bm', 'Bambara'), + ('eu', 'Basque'), + ('be', 'Belarusian'), + ('bn', 'Bengali'), + ('bh', 'Bihari languages'), + ('bi', 'Bislama'), + ('bo', 'Tibetan'), + ('bs', 'Bosnian'), + ('br', 'Breton'), + ('bg', 'Bulgarian'), + ('my', 'Burmese'), + ('ca', 'Catalan; Valencian'), + ('cs', 'Czech'), + ('ch', 'Chamorro'), + ('ce', 'Chechen'), + ('zh', 'Chinese'), + ('cu', 'Church Slavic'), + ('cv', 'Chuvash'), + ('kw', 'Cornish'), + ('co', 'Corsican'), + ('cr', 'Cree'), + ('cy', 'Welsh'), + ('cs', 'Czech'), + ('da', 'Danish'), + ('de', 'German'), + ('dv', 'Divehi; Dhivehi; Maldivian'), + ('nl', 'Dutch; Flemish'), + ('dz', 'Dzongkha'), + ('el', 'Greek'), + ('en', 'English'), + ('eo', 'Esperanto'), # <3 + ('et', 'Estonian'), + ('eu', 'Basque'), + ('ee', 'Ewe'), + ('fo', 'Faroese'), + ('fa', 'Persian'), + ('fj', 'Fijian'), + ('fi', 'Finnish'), + ('fr', 'French'), + ('fy', 'Western Frisian'), + ('ff', 'Fulah'), + ('ga', 'Georgian'), + ('de', 'German'), + ('gd', 'Gaelic; Scottish Gaelic'), + ('ga', 'Irish'), + ('gl', 'Galician'), + ('gv', 'Manx'), + ('gn', 'Guarani'), + ('gu', 'Gujarati'), + ('ht', 'Haitian; Haitian Creole'), + ('ha', 'Hausa'), + ('he', 'Hebrew'), + ('hz', 'Herero'), + ('hi', 'Hindi'), + ('ho', 'Hiri Motu'), + ('hr', 'Croatian'), + ('hu', 'Hungarian'), + ('hy', 'Armenian'), + ('ig', 'Igbo'), + ('is', 'Icelandic'), + ('io', 'Ido'), + ('ii', 'Sichuan Yi; Nuosu'), + ('iu', 'Inuktitut'), + ('ie', 'Interlingue; Occidental'), + ('ia', 'Interlingua'), # !? + ('id', 'Indonesian'), + ('ik', 'Inupiaq'), + ('is', 'Icelandic'), + ('it', 'Italian'), + ('jv', 'Javanese'), + ('ja', 'Japanese'), + ('kl', 'Kalaallisut; Greenlandic'), + ('kn', 'Kannada'), + ('ks', 'Kashmiri'), + ('ka', 'Georgian'), + ('kr', 'Kanuri'), + ('kk', 'Kazakh'), + ('km', 'Central Khmer'), + ('ki', 'Kikuyu; Gikuyu'), + ('rw', 'Kinyarwanda'), + ('ky', 'Kirghiz; Kyrgyz'), + ('kv', 'Komi'), + ('kg', 'Kongo'), + ('ko', 'Korean'), + ('kj', 'Kuanyama; Kwanyama'), + ('ku', 'Kurdish'), + ('lo', 'Lao'), + ('la', 'Latin'), + ('lv', 'Latvian'), + ('li', 'Limburgan; Limburger; Limburgish'), + ('ln', 'Lingala'), + ('lt', 'Lithuanian'), + ('lb', 'Luxembourgish; Letzeburgesch'), + ('lu', 'Luba-Katanga'), + ('lg', 'Ganda'), + ('ma', 'Marain'), # _[⋅.-]_ + ('mk', 'Macedonian'), + ('mh', 'Marshallese'), + ('ml', 'Malayalam'), + ('mi', 'Maori'), + ('mr', 'Marathi'), + ('ms', 'Malay'), + ('Mi', 'Micmac'), + ('mg', 'Malagasy'), + ('mt', 'Maltese'), + ('mn', 'Mongolian'), + ('mi', 'Maori'), + ('ms', 'Malay'), + ('my', 'Burmese'), + ('na', 'Nauru'), + ('nv', 'Navajo; Navaho'), + ('nr', 'South Ndebele'), + ('nd', 'North Ndebele'), + ('ng', 'Ndonga'), + ('ne', 'Nepali'), + ('nl', 'Dutch; Flemish'), + ('nn', 'Norwegian Nynorsk; Nynorsk, Norwegian'), + ('nb', 'Bokmål, Norwegian; Norwegian Bokmål'), + ('no', 'Norwegian'), + ('oc', 'Occitan (post 1500)'), + ('oj', 'Ojibwa'), + ('or', 'Oriya'), + ('om', 'Oromo'), + ('os', 'Ossetian; Ossetic'), + ('pa', 'Panjabi; Punjabi'), + ('fa', 'Persian'), + ('pi', 'Pali'), + ('pl', 'Polish'), + ('pt', 'Portuguese'), + ('ps', 'Pushto; Pashto'), + ('qu', 'Quechua'), + ('rm', 'Romansh'), + ('ro', 'Romanian; Moldavian; Moldovan'), + ('ro', 'Romanian; Moldavian; Moldovan'), + ('rn', 'Rundi'), + ('ru', 'Russian'), + ('sg', 'Sango'), + ('sa', 'Sanskrit'), + ('si', 'Sinhala; Sinhalese'), + ('sk', 'Slovak'), + ('sk', 'Slovak'), + ('sl', 'Slovenian'), + ('se', 'Northern Sami'), + ('sm', 'Samoan'), + ('sn', 'Shona'), + ('sd', 'Sindhi'), + ('so', 'Somali'), + ('st', 'Sotho, Southern'), + ('es', 'Spanish; Castilian'), + ('sq', 'Albanian'), + ('sc', 'Sardinian'), + ('sr', 'Serbian'), + ('ss', 'Swati'), + ('su', 'Sundanese'), + ('sw', 'Swahili'), + ('sv', 'Swedish'), + ('ty', 'Tahitian'), + ('ta', 'Tamil'), + ('tt', 'Tatar'), + ('te', 'Telugu'), + ('tg', 'Tajik'), + ('tl', 'Tagalog'), + ('th', 'Thai'), + ('bo', 'Tibetan'), + ('ti', 'Tigrinya'), + ('to', 'Tonga (Tonga Islands)'), + ('tn', 'Tswana'), + ('ts', 'Tsonga'), + ('tk', 'Turkmen'), + ('tr', 'Turkish'), + ('tw', 'Twi'), + ('ug', 'Uighur; Uyghur'), + ('uk', 'Ukrainian'), + ('ur', 'Urdu'), + ('uz', 'Uzbek'), + ('ve', 'Venda'), + ('vi', 'Vietnamese'), + ('vo', 'Volapük'), + ('cy', 'Welsh'), + ('wa', 'Walloon'), + ('wo', 'Wolof'), + ('xh', 'Xhosa'), + ('yi', 'Yiddish'), + ('yo', 'Yoruba'), + ('za', 'Zhuang; Chuang'), + ('zh', 'Chinese'), + ('zu', 'Zulu') +] + def guess_language(context): - return 'fr' # FIXME use context.tags + for language in languages: + if language[0] in context.tags: + return language[0] + raise Exception("No language found. Use a tag @en or @fr?") From 88189cef6027eb692f88186b36b4e08d4a6d660a Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 19:11:39 +0200 Subject: [PATCH 22/53] refacto(naming): esoteric vs exoteric steps Esoteric steps run from the inside and access the database directly. Exoteric steps run from the outside and run REST requests. --- .../{steps_scrutin_crud.py => steps_esoteric_scrutin.py} | 7 ++++++- .../steps/{steps_user_crud.py => steps_esoteric_user.py} | 7 ++++++- tests/acceptance/steps/{tools_db.py => tools_dbal.py} | 0 3 files changed, 12 insertions(+), 2 deletions(-) rename tests/acceptance/steps/{steps_scrutin_crud.py => steps_esoteric_scrutin.py} (87%) rename tests/acceptance/steps/{steps_user_crud.py => steps_esoteric_user.py} (88%) rename tests/acceptance/steps/{tools_db.py => tools_dbal.py} (100%) diff --git a/tests/acceptance/steps/steps_scrutin_crud.py b/tests/acceptance/steps/steps_esoteric_scrutin.py similarity index 87% rename from tests/acceptance/steps/steps_scrutin_crud.py rename to tests/acceptance/steps/steps_esoteric_scrutin.py index 369b59d..7d250e3 100644 --- a/tests/acceptance/steps/steps_scrutin_crud.py +++ b/tests/acceptance/steps/steps_esoteric_scrutin.py @@ -1,3 +1,8 @@ +""" +Esoteric (coming from within) steps about polls. +These steps deal with the database directly, and NOT with the REST API. +""" + ############################################################################### # from behave import given, when, then # Pycharm 2020.1 won't understand the above, but it understands `step`. @@ -10,7 +15,7 @@ from hamcrest import assert_that, equal_to from tools_nlp import parse_amount, parse_yaml -from tools_db import count_polls +from tools_dbal import count_polls ############################################################################### diff --git a/tests/acceptance/steps/steps_user_crud.py b/tests/acceptance/steps/steps_esoteric_user.py similarity index 88% rename from tests/acceptance/steps/steps_user_crud.py rename to tests/acceptance/steps/steps_esoteric_user.py index b310343..41288eb 100644 --- a/tests/acceptance/steps/steps_user_crud.py +++ b/tests/acceptance/steps/steps_esoteric_user.py @@ -1,3 +1,8 @@ +""" +Esoteric (coming from within) steps about users. +These steps deal with the database directly, and NOT with the REST API. +""" + ############################################################################### # from behave import given, when, then # Pycharm 2020.1 won't understand the above, but it understands `step`. @@ -8,7 +13,7 @@ from hamcrest import assert_that, equal_to from tools_nlp import parse_amount -from tools_db import count_users +from tools_dbal import count_users ############################################################################### diff --git a/tests/acceptance/steps/tools_db.py b/tests/acceptance/steps/tools_dbal.py similarity index 100% rename from tests/acceptance/steps/tools_db.py rename to tests/acceptance/steps/tools_dbal.py From b25b0625d41189504462471c18ccd749352a101c Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 19:20:19 +0200 Subject: [PATCH 23/53] chore(review) --- tests/acceptance/environment.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/acceptance/environment.py b/tests/acceptance/environment.py index 96b305d..6e8f422 100644 --- a/tests/acceptance/environment.py +++ b/tests/acceptance/environment.py @@ -3,29 +3,21 @@ https://behave.readthedocs.io/en/latest/api.html#environment-file-functions """ - -# Since we expect most of our step defs to require the flexibity of regexes, -# we make regular expressions the default. (I18N, epicene) +# Since we expect most of our step defs to require the flexibility of regular +# expressions (I18N, epicene) we make regular expressions the default. # This is at the expense of automatic type casting of step variables. -# It's a somewhat good tradeoff since natural language numbers still do require -# explicit casting in all matchers (AFAIK in early 2020). from behave import use_step_matcher use_step_matcher("re") # You can still override this right before your step def if you want, # by calling use_step_matcher() with one of the following: # "parse" (factory default), "cfparse" -# and calling it again with "re" after your step def. (it uses a `global`) - - -# def _log(*args): -# print(*args) # hook to logger instead +# and calling it again with "re" after your step def. (it uses a `global`) def before_feature(context, feature): """ Ran before _each_ feature file is exercised. """ - # _log("Before feature", feature) pass # context.fixtures = ['behave-fixtures.json'] @@ -34,7 +26,6 @@ def before_scenario(context, scenario): """ Ran before _each_ scenario is run. """ - # _log("Before scenario", scenario) pass # context.fixtures.append('behave-second-fixture.json') @@ -47,5 +38,4 @@ def before_all(context): def django_ready(context): - # _log("Django Ready", context) context.django = True From 3a62ea6111d979d7257d6e52fa7cd749402e1553 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 27 Apr 2020 19:23:44 +0200 Subject: [PATCH 24/53] feat(features): allow `???` as dummy step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spanish is going to be fun, I can tell. ¿que? ¿cuando se come aqui? --- tests/acceptance/steps/steps_poetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/steps/steps_poetry.py b/tests/acceptance/steps/steps_poetry.py index 5e2c8ce..de4d4e5 100644 --- a/tests/acceptance/steps/steps_poetry.py +++ b/tests/acceptance/steps/steps_poetry.py @@ -6,7 +6,7 @@ from behave import step -@step(u"etc[.]?|…|[.]{2,}") +@step(u"(?:etc[.]?|…|[.]{3,}|[?!]+)[?!]*") def et_caetera(context): pass From daf15a79f85655e86d31154a500e8a0c3fc04745 Mon Sep 17 00:00:00 2001 From: domi41 Date: Tue, 28 Apr 2020 05:20:33 +0200 Subject: [PATCH 25/53] docs(features): document the ugly business with PyCharm We sacrifice usage of given when then wrappers, for now. That has to go away at some point. Note that in 2016 it was working. Perhaps it's my own computer at fault somehow. Open an issue or write in the README if you gather your own metrics about this ! --- tests/acceptance/steps/README.md | 35 +++++++++++++++++++ .../steps/steps_esoteric_scrutin.py | 10 +----- tests/acceptance/steps/steps_esoteric_user.py | 8 +---- 3 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 tests/acceptance/steps/README.md diff --git a/tests/acceptance/steps/README.md b/tests/acceptance/steps/README.md new file mode 100644 index 0000000..1930550 --- /dev/null +++ b/tests/acceptance/steps/README.md @@ -0,0 +1,35 @@ + +# Step Definitions and Tooling + +Steps can be `esoteric` and access the inside of the application, +or `exoteric` and access the application from outside, from REST. + + +## Caveats + +### Usage of the generic `@step` def wrapper + +We are using: + +```python +from behave import step +``` + +We could use: + +```python +from behave import given, when, then +``` + +But PyCharm 2020.1 won't understand the above. +It does understand `step`. + +During the initial sprint, the IDE sugar was deemed more important. +Feel free to open an issue to discuss and|or re-evaluate this. + + +### I18N of error messages + +Hamcrest has no I18N support whatsoever: +https://github.com/hamcrest/PyHamcrest/blob/632840d9ffe7fd4e9ea9ad6ac1db9ff3871cb984/src/hamcrest/core/assert_that.py#L65 + diff --git a/tests/acceptance/steps/steps_esoteric_scrutin.py b/tests/acceptance/steps/steps_esoteric_scrutin.py index 7d250e3..8393439 100644 --- a/tests/acceptance/steps/steps_esoteric_scrutin.py +++ b/tests/acceptance/steps/steps_esoteric_scrutin.py @@ -3,15 +3,7 @@ These steps deal with the database directly, and NOT with the REST API. """ -############################################################################### -# from behave import given, when, then -# Pycharm 2020.1 won't understand the above, but it understands `step`. -from behave import step -from yaml import safe_load -# Swap to usage of `given`, `when`, `then` when relevant -############################################################################### - - +from behave import given, when, then, step from hamcrest import assert_that, equal_to from tools_nlp import parse_amount, parse_yaml diff --git a/tests/acceptance/steps/steps_esoteric_user.py b/tests/acceptance/steps/steps_esoteric_user.py index 41288eb..7816070 100644 --- a/tests/acceptance/steps/steps_esoteric_user.py +++ b/tests/acceptance/steps/steps_esoteric_user.py @@ -3,13 +3,7 @@ These steps deal with the database directly, and NOT with the REST API. """ -############################################################################### -# from behave import given, when, then -# Pycharm 2020.1 won't understand the above, but it understands `step`. -from behave import step -# Swap to usage of `given`, `when`, `then` when relevant -############################################################################### - +from behave import given, when, then, step from hamcrest import assert_that, equal_to from tools_nlp import parse_amount From df57105a7fd1efe434e507ab10bed255a311c289 Mon Sep 17 00:00:00 2001 From: domi41 Date: Tue, 28 Apr 2020 05:37:25 +0200 Subject: [PATCH 26/53] chore(features): trick the bug tracker, perhaps --- tests/acceptance/steps/steps_poetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/steps/steps_poetry.py b/tests/acceptance/steps/steps_poetry.py index de4d4e5..b5276cc 100644 --- a/tests/acceptance/steps/steps_poetry.py +++ b/tests/acceptance/steps/steps_poetry.py @@ -8,9 +8,9 @@ @step(u"(?:etc[.]?|…|[.]{3,}|[?!]+)[?!]*") def et_caetera(context): - pass + pass # nothing is cool @step(u"ce n'est pas tout *!*:?") def but_wait_there_is_more(context): - pass + pass # nothing is cool From 5bf58f89ac576dd4f275c3ac7eda9eb50f45f5e3 Mon Sep 17 00:00:00 2001 From: domi41 Date: Tue, 28 Apr 2020 05:38:04 +0200 Subject: [PATCH 27/53] chore(features): remove fixtures code snippets --- tests/acceptance/environment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/acceptance/environment.py b/tests/acceptance/environment.py index 6e8f422..fc78433 100644 --- a/tests/acceptance/environment.py +++ b/tests/acceptance/environment.py @@ -19,7 +19,6 @@ def before_feature(context, feature): Ran before _each_ feature file is exercised. """ pass - # context.fixtures = ['behave-fixtures.json'] def before_scenario(context, scenario): @@ -27,7 +26,6 @@ def before_scenario(context, scenario): Ran before _each_ scenario is run. """ pass - # context.fixtures.append('behave-second-fixture.json') def before_all(context): From a64b96516eec672901c7e8163f54e011c8f2e68b Mon Sep 17 00:00:00 2001 From: domi41 Date: Tue, 28 Apr 2020 07:45:16 +0200 Subject: [PATCH 28/53] feat(features): share some context between step defs Note: Shared context variables are tempting but ought to be scarce and carefully though of, since they often incur inertia and creep. --- tests/acceptance/environment.py | 5 +++++ tests/acceptance/steps/context_main.py | 16 ++++++++++++++++ tests/acceptance/steps/steps_esoteric_scrutin.py | 3 ++- tests/acceptance/steps/steps_esoteric_user.py | 3 ++- tests/acceptance/steps/steps_poetry.py | 2 +- 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 tests/acceptance/steps/context_main.py diff --git a/tests/acceptance/environment.py b/tests/acceptance/environment.py index fc78433..27ac5ef 100644 --- a/tests/acceptance/environment.py +++ b/tests/acceptance/environment.py @@ -3,6 +3,10 @@ https://behave.readthedocs.io/en/latest/api.html#environment-file-functions """ + +from steps.context_main import reset_context as reset_main_context + + # Since we expect most of our step defs to require the flexibility of regular # expressions (I18N, epicene) we make regular expressions the default. # This is at the expense of automatic type casting of step variables. @@ -25,6 +29,7 @@ def before_scenario(context, scenario): """ Ran before _each_ scenario is run. """ + reset_main_context(context) pass diff --git a/tests/acceptance/steps/context_main.py b/tests/acceptance/steps/context_main.py new file mode 100644 index 0000000..229c5e5 --- /dev/null +++ b/tests/acceptance/steps/context_main.py @@ -0,0 +1,16 @@ +""" +What we need: +- Share context between step defs (including from different files) +- Easy and scalable context reset on each scenario +- Multiple contexts (do we?) + +Context variables in themselves must be as scarce as possible, +and only added after careful consideration, though. + +So far we're using the PatchedContext instance provided by behave. +""" + + +def reset_context(context): + context.that_user = None + context.that_poll = None diff --git a/tests/acceptance/steps/steps_esoteric_scrutin.py b/tests/acceptance/steps/steps_esoteric_scrutin.py index 8393439..b902d73 100644 --- a/tests/acceptance/steps/steps_esoteric_scrutin.py +++ b/tests/acceptance/steps/steps_esoteric_scrutin.py @@ -14,7 +14,7 @@ # @given -@step(u"un(?: autre)? scrutin comme suit:?") +@step(u"un(?: autre)? scrutin comme suit *:?") def there_is_a_poll_like_so(context): data = parse_yaml(context) from election.models import Election @@ -23,6 +23,7 @@ def there_is_a_poll_like_so(context): election.candidates = data.get('candidates') election.num_grades = data.get('grades', 7) election.save() + context.that_poll = election # @then diff --git a/tests/acceptance/steps/steps_esoteric_user.py b/tests/acceptance/steps/steps_esoteric_user.py index 7816070..9f29b9b 100644 --- a/tests/acceptance/steps/steps_esoteric_user.py +++ b/tests/acceptance/steps/steps_esoteric_user.py @@ -25,11 +25,12 @@ def create_citizen_named(context, name): print("Creating citizen named `%s'…" % name) from django.contrib.auth.models import User - _user = User.objects.create_user( + user = User.objects.create_user( username=name, email='user@test.mieuxvoter.fr', password=name ) + context.that_user = user # @then diff --git a/tests/acceptance/steps/steps_poetry.py b/tests/acceptance/steps/steps_poetry.py index b5276cc..6b87c51 100644 --- a/tests/acceptance/steps/steps_poetry.py +++ b/tests/acceptance/steps/steps_poetry.py @@ -12,5 +12,5 @@ def et_caetera(context): @step(u"ce n'est pas tout *!*:?") -def but_wait_there_is_more(context): +def wait_there_is_more(context): pass # nothing is cool From f2a7bf67be2bba1560875e16e63c9bbb0b5e2c8d Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 07:40:14 +0200 Subject: [PATCH 29/53] feat(feature): add a tool to find a user This may eventually support email and id. --- tests/acceptance/steps/tools_dbal.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/acceptance/steps/tools_dbal.py b/tests/acceptance/steps/tools_dbal.py index d453bda..9eedfba 100644 --- a/tests/acceptance/steps/tools_dbal.py +++ b/tests/acceptance/steps/tools_dbal.py @@ -15,3 +15,8 @@ def count_polls(): # TBD: "scrutin" translates to "poll"? return Election.objects.count() +def find_user(identifier): + user = User.objects.get(username=identifier) + if user is not None: + return user + raise ValueError("No user found matching `%s`." % identifier) From 3b33b0d13fa4f03eb3ca441a01c650d4160bb995 Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 07:56:37 +0200 Subject: [PATCH 30/53] chore(typo) --- tests/acceptance/features/fr/10.creer-un-scrutin.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/acceptance/features/fr/10.creer-un-scrutin.feature b/tests/acceptance/features/fr/10.creer-un-scrutin.feature index a93e175..de02573 100644 --- a/tests/acceptance/features/fr/10.creer-un-scrutin.feature +++ b/tests/acceptance/features/fr/10.creer-un-scrutin.feature @@ -20,7 +20,7 @@ Contexte: @wip Scénario: Créer un scrutin au jugement majoritaire - Quand ??? créé un scrutin comme suit: + Quand ??? crée un scrutin comme suit: """ titre: ??? candidats: From ec710b8a784b1082032dff067c3fe0a398cb9edc Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 08:10:57 +0200 Subject: [PATCH 31/53] feat(features): add a step to print the mentioned user Behave is tricky to configure in order to show the output. This step does not yield very useful information for now, but it allows us to debug and experiment with behave conf. --- tests/acceptance/steps/steps_ouputting.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/acceptance/steps/steps_ouputting.py diff --git a/tests/acceptance/steps/steps_ouputting.py b/tests/acceptance/steps/steps_ouputting.py new file mode 100644 index 0000000..975c664 --- /dev/null +++ b/tests/acceptance/steps/steps_ouputting.py @@ -0,0 +1,14 @@ +""" +These steps should [ae]ffect nothing. +They help keep the features idiomatic, engaging, etc. +""" + +from behave import step +from tools_dbal import find_user + + +@step(u"j(?:e |')(?:débogue|affiche)(?: l[ea])? citoyen(?:[⋅.-]?ne|)(?: nommé(?:[⋅.-]?e|))? (?P.+)") +@step(u"I print(?: the)? user(?: named)? (?P.+)") +def print_user(context, name): + user = find_user(name) + print(user) From 26c7dcc405fa3004f72dca42250544f143ef0ed6 Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 10:27:43 +0200 Subject: [PATCH 32/53] feat(features): show usage example of Django test client --- election/urls.py | 0 .../fr/00.deboguer-les-etapes.feature | 13 ++++++- tests/acceptance/steps/steps_rest_scrutin.py | 38 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 election/urls.py create mode 100644 tests/acceptance/steps/steps_rest_scrutin.py diff --git a/election/urls.py b/election/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature index 8a99acf..e45b246 100644 --- a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr/00.deboguer-les-etapes.feature @@ -50,4 +50,15 @@ Scénario: Compter les scrutins Alors il devrait maintenant y avoir deux scrutins dans la base de données -# Scénario: Soumettre un nouveau scrutin +@new +Scénario: Soumettre un nouveau scrutin + Quand quelqu'un crée un scrutin comme suit: + """ + titre: Les Histoires Canines + candidats: + - Milou + - Laika + - Cerbère + - Lassie + """ + Alors il devrait maintenant y avoir un scrutin dans la base de données diff --git a/tests/acceptance/steps/steps_rest_scrutin.py b/tests/acceptance/steps/steps_rest_scrutin.py new file mode 100644 index 0000000..bb9d601 --- /dev/null +++ b/tests/acceptance/steps/steps_rest_scrutin.py @@ -0,0 +1,38 @@ +""" +REST steps about polls. +These steps deal with the REST API, not the database. +""" + +from behave import given, when, then, step +from hamcrest import assert_that, equal_to + +from tools_nlp import parse_amount, parse_yaml +from tools_dbal import count_polls + + +############################################################################### + + +# @given +@step(u"(?P.+) crée un(?: autre)? scrutin comme suit *:?") +def actor_creates_a_poll_like_so(context, actor): + data = parse_yaml(context) + + # How about this instead? + # actor = parse_actor(context, actor) + # actor.post('/polls', data={ + # 'title': data.get('title'), + # 'candidates': data.get('candidates'), + # 'num_grades': data.get('grades', 7), + # }) + + from django.test import Client + response = Client().post('/api/election/polls', { + 'title': data.get('title'), + 'candidates': data.get('candidates'), + 'num_grades': data.get('grades', 7), + }) + + print(response.status_code) + print(response.content) + From 0be4c9007239aec26b86884fed030588cae31d9b Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 11:11:24 +0200 Subject: [PATCH 33/53] feat(features): add Actors Actors provide a handy layer over requests. Especially useful once authentication kicks in. Right now they are empty and hacky, but the idea is there. --- tests/acceptance/steps/steps_rest_scrutin.py | 22 +++--------- tests/acceptance/steps/toolbox.py | 38 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 tests/acceptance/steps/toolbox.py diff --git a/tests/acceptance/steps/steps_rest_scrutin.py b/tests/acceptance/steps/steps_rest_scrutin.py index bb9d601..b0c48c6 100644 --- a/tests/acceptance/steps/steps_rest_scrutin.py +++ b/tests/acceptance/steps/steps_rest_scrutin.py @@ -6,33 +6,19 @@ from behave import given, when, then, step from hamcrest import assert_that, equal_to -from tools_nlp import parse_amount, parse_yaml -from tools_dbal import count_polls +from toolbox import parse_actor, parse_yaml ############################################################################### -# @given +# @when @step(u"(?P.+) crée un(?: autre)? scrutin comme suit *:?") def actor_creates_a_poll_like_so(context, actor): data = parse_yaml(context) - - # How about this instead? - # actor = parse_actor(context, actor) - # actor.post('/polls', data={ - # 'title': data.get('title'), - # 'candidates': data.get('candidates'), - # 'num_grades': data.get('grades', 7), - # }) - - from django.test import Client - response = Client().post('/api/election/polls', { + actor = parse_actor(context, actor) + actor.post('/polls', data={ 'title': data.get('title'), 'candidates': data.get('candidates'), 'num_grades': data.get('grades', 7), }) - - print(response.status_code) - print(response.content) - diff --git a/tests/acceptance/steps/toolbox.py b/tests/acceptance/steps/toolbox.py new file mode 100644 index 0000000..bf22a86 --- /dev/null +++ b/tests/acceptance/steps/toolbox.py @@ -0,0 +1,38 @@ +from django.test import Client + +from tools_nlp import * + + +class Actor(object): + + def __init__(self, name=None) -> None: + super().__init__() + self.name = name + self.client = Client() + + def adjust_path(self, path): + if path.startswith('/'): + path = path[1:] + return "/api/election/%s" % path + + def handle_possible_failure(self, method, path, response): + if response.status_code >= 400: + print("%s %s (%d)" % (method, path, response.status_code)) + print(response.content) + return response + + def post(self, path, data, safe_to_fail=False): + path = self.adjust_path(path) + response = self.client.post(path=path, data=data) + # response = self.client.generic(path=path, method="POST", data=data) + + if not safe_to_fail: + self.handle_possible_failure('POST', path, response) + + +def parse_actor(context, actor_name): + if 'actors' not in context: + context.actors = dict() + if actor_name not in context.actors: + context.actors[actor_name] = Actor(name=actor_name) + return context.actors[actor_name] From c79c1af99530f83ba2e34a5966d9f960c386e1a8 Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 18:19:28 +0200 Subject: [PATCH 34/53] doc(features): explicit the intent of the toolbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just experimenting this pattern, to see if step defs get prettier. … And what's the cost. --- tests/acceptance/steps/toolbox.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/acceptance/steps/toolbox.py b/tests/acceptance/steps/toolbox.py index bf22a86..f2d2374 100644 --- a/tests/acceptance/steps/toolbox.py +++ b/tests/acceptance/steps/toolbox.py @@ -1,3 +1,8 @@ +""" +The intent is for toolbox to be a sort of shortcut, providing most tools. +Not sure this is the python way. +""" + from django.test import Client from tools_nlp import * From 231e1f0fba43179af18ef7bc79ff36f11a246035 Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 18:30:29 +0200 Subject: [PATCH 35/53] conf(features): Tweak behave some more. Make sure when using print() in step defs to end it with a `\n`, This is a pitfall of behave, I guess? We can set the format to plain, using the --format plain CLI option, since the `format = plain` in the `.behaverc` changes nothing. When in plain format, there are no colors and our print() appears. We tried behave 1.2.6 and 1.2.7-dev2. This may well be solved by some tweaking. Found out about `aloe`, and `pytest-bdd`. This may well be solved by some experimenting. --- .behaverc | 31 +++++++++---------- tests/acceptance/steps/steps_esoteric_user.py | 2 +- tests/acceptance/steps/steps_ouputting.py | 2 +- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/.behaverc b/.behaverc index 5892f9b..6885a58 100644 --- a/.behaverc +++ b/.behaverc @@ -1,21 +1,20 @@ [behave] -; perhaps we want some of those? -;format=plain -;logging_clear_handlers=yes -;logging_filter=-suds +; We are not using the default path to leave room for unit tests ; also, apps. +paths = tests/acceptance -; We are not using the default path to leave room for unit tests? -paths=tests/acceptance + +# What is going on? Does this change anything for you? --format=plain works +;format = plain + +; Perhaps we want some of those? +;logging_clear_handlers = yes +;logging_filter = -suds ; Show all print() statements even if tests pass. (bugs! bugs!) -;stderr_capture=False -;stdout_capture=False +;stderr_capture = False +stdout_capture = False +;log_capture = False -; Skipped projects clutter the console when using tags such as : -; $ python manage.py behave --tags=wip -; You can add a tag to a feature by using its name as descriptor : -; @wip -; Feature: Working hard -; [...] -; You can also add tags to Scenarios ! -show_skipped=False \ No newline at end of file +show_skipped = False +show_snippets = False +summary = True \ No newline at end of file diff --git a/tests/acceptance/steps/steps_esoteric_user.py b/tests/acceptance/steps/steps_esoteric_user.py index 9f29b9b..41135aa 100644 --- a/tests/acceptance/steps/steps_esoteric_user.py +++ b/tests/acceptance/steps/steps_esoteric_user.py @@ -23,7 +23,7 @@ # @given @step(u"un(?:[⋅.-]?e|) citoyen(?:[⋅.-]?ne|) nommé(?:[⋅.-]?e|) (?P.+)") def create_citizen_named(context, name): - print("Creating citizen named `%s'…" % name) + print("Creating citizen named `%s'…\n" % name) from django.contrib.auth.models import User user = User.objects.create_user( username=name, diff --git a/tests/acceptance/steps/steps_ouputting.py b/tests/acceptance/steps/steps_ouputting.py index 975c664..7adc9b3 100644 --- a/tests/acceptance/steps/steps_ouputting.py +++ b/tests/acceptance/steps/steps_ouputting.py @@ -11,4 +11,4 @@ @step(u"I print(?: the)? user(?: named)? (?P.+)") def print_user(context, name): user = find_user(name) - print(user) + print("%s\n" % user) From 0ed8c027b7467dcc7f468937c7c8c4c13730cd22 Mon Sep 17 00:00:00 2001 From: domi41 Date: Wed, 29 Apr 2020 18:57:30 +0200 Subject: [PATCH 36/53] refacto(features): hide some more into the dbal --- tests/acceptance/steps/steps_esoteric_user.py | 9 ++------- tests/acceptance/steps/tools_dbal.py | 8 ++++++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/acceptance/steps/steps_esoteric_user.py b/tests/acceptance/steps/steps_esoteric_user.py index 41135aa..1a8206b 100644 --- a/tests/acceptance/steps/steps_esoteric_user.py +++ b/tests/acceptance/steps/steps_esoteric_user.py @@ -7,7 +7,7 @@ from hamcrest import assert_that, equal_to from tools_nlp import parse_amount -from tools_dbal import count_users +from tools_dbal import count_users, make_user ############################################################################### @@ -24,12 +24,7 @@ @step(u"un(?:[⋅.-]?e|) citoyen(?:[⋅.-]?ne|) nommé(?:[⋅.-]?e|) (?P.+)") def create_citizen_named(context, name): print("Creating citizen named `%s'…\n" % name) - from django.contrib.auth.models import User - user = User.objects.create_user( - username=name, - email='user@test.mieuxvoter.fr', - password=name - ) + user = make_user(context, name) context.that_user = user diff --git a/tests/acceptance/steps/tools_dbal.py b/tests/acceptance/steps/tools_dbal.py index 9eedfba..a976080 100644 --- a/tests/acceptance/steps/tools_dbal.py +++ b/tests/acceptance/steps/tools_dbal.py @@ -7,6 +7,14 @@ from election.models import Election +def make_user(context, username): + return User.objects.create_user( + username=username, + email='user@test.mieuxvoter.fr', + password=username + ) + + def count_users(): return User.objects.count() From e03c7036b59dc49f2cce1cba0cc65367b235cdc6 Mon Sep 17 00:00:00 2001 From: domi41 Date: Thu, 30 Apr 2020 12:50:42 +0200 Subject: [PATCH 37/53] feat(features): Print a citizen Printing is not as good in behave as it is in other runners, at least from an end-user perspective. --- tests/acceptance/features/fr_FR | 1 + 1 file changed, 1 insertion(+) create mode 160000 tests/acceptance/features/fr_FR diff --git a/tests/acceptance/features/fr_FR b/tests/acceptance/features/fr_FR new file mode 160000 index 0000000..91486c9 --- /dev/null +++ b/tests/acceptance/features/fr_FR @@ -0,0 +1 @@ +Subproject commit 91486c99175021af6c595043b59ed56ca5f96632 From 576d66dc48e440a6a27f75caee17358645dbacec Mon Sep 17 00:00:00 2001 From: domi41 Date: Thu, 30 Apr 2020 12:50:42 +0200 Subject: [PATCH 38/53] feat(features): Print a citizen Printing is not as good in behave as it is in other runners, at least from an end-user perspective. --- tests/acceptance/features/fr_FR | 1 - .../00.deboguer-les-etapes.feature | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) delete mode 160000 tests/acceptance/features/fr_FR rename tests/acceptance/features/{fr => fr_FR}/00.deboguer-les-etapes.feature (86%) diff --git a/tests/acceptance/features/fr_FR b/tests/acceptance/features/fr_FR deleted file mode 160000 index 91486c9..0000000 --- a/tests/acceptance/features/fr_FR +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 91486c99175021af6c595043b59ed56ca5f96632 diff --git a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature similarity index 86% rename from tests/acceptance/features/fr/00.deboguer-les-etapes.feature rename to tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature index e45b246..d925c55 100644 --- a/tests/acceptance/features/fr/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature @@ -6,15 +6,17 @@ Fonctionnalité: Décrire les comportements des étapes des scénarios Nous souhaitons les utiliser dans différents contextes + Scénario: Requérir la présence à priori de citoyen⋅nes # New keyword Sachant has not yet propagated to our behave version - #Sachant qu'il ne devrait y avoir aucun citoyen dans la base de données +# Sachant qu'il ne devrait y avoir aucun citoyen dans la base de données # So we use the less idiomatic equivalent Étant donné qu'il ne devrait y avoir aucun citoyen dans la base de données Étant donné un citoyen nommé Michel Balinski Alors il devrait y avoir un citoyen dans la base de données + Scénario: Requérir la présence à priori de scrutins Sachant qu'il ne devrait y avoir aucun scrutin dans la base de données Étant donné un scrutin comme suit: @@ -28,11 +30,12 @@ Scénario: Requérir la présence à priori de scrutins Alors il devrait y avoir un scrutin dans la base de données + Scénario: Compter les scrutins Sachant qu'il ne devrait y avoir aucun scrutin dans la base de données Étant donné un scrutin comme suit: """ - titre: Mon app JM préférée + titre: Application JM préférée candidats: - app.mieuxvoter.fr - jugementmajoritaire.net @@ -42,7 +45,7 @@ Scénario: Compter les scrutins Mais ce n'est pas tout ! Étant donné un autre scrutin comme suit: """ - titre: Canaux de communication interne + titre: Canal de communication interne candidats: - Telegram - Telegram @@ -50,7 +53,7 @@ Scénario: Compter les scrutins Alors il devrait maintenant y avoir deux scrutins dans la base de données -@new + Scénario: Soumettre un nouveau scrutin Quand quelqu'un crée un scrutin comme suit: """ @@ -62,3 +65,10 @@ Scénario: Soumettre un nouveau scrutin - Lassie """ Alors il devrait maintenant y avoir un scrutin dans la base de données + + + +@new +Scénario: Afficher un citoyen + Étant donné un citoyen nommé Michel Balinski + Alors je débogue le citoyen nommé Michel Balinski From d6128368c80471c7f6916765617aacbfd40fd259 Mon Sep 17 00:00:00 2001 From: domi41 Date: Thu, 30 Apr 2020 12:51:43 +0200 Subject: [PATCH 39/53] fix(features): use the complete locale tag, eg: `@fr_FR` --- tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature | 3 ++- .../features/{fr => fr_FR}/10.creer-un-scrutin.feature | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename tests/acceptance/features/{fr => fr_FR}/10.creer-un-scrutin.feature (99%) diff --git a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature index d925c55..c6273e6 100644 --- a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature @@ -1,5 +1,6 @@ #language: fr -@fr +@fr_FR +@vigil Fonctionnalité: Décrire les comportements des étapes des scénarios Dans le but d'expliciter le comportement des étapes des scénarios En tant que débogueur⋅es diff --git a/tests/acceptance/features/fr/10.creer-un-scrutin.feature b/tests/acceptance/features/fr_FR/10.creer-un-scrutin.feature similarity index 99% rename from tests/acceptance/features/fr/10.creer-un-scrutin.feature rename to tests/acceptance/features/fr_FR/10.creer-un-scrutin.feature index de02573..12b45f3 100644 --- a/tests/acceptance/features/fr/10.creer-un-scrutin.feature +++ b/tests/acceptance/features/fr_FR/10.creer-un-scrutin.feature @@ -1,5 +1,5 @@ #language: fr -@fr +@fr_FR Fonctionnalité: Créer un scrutin au jugement majoritaire sur app.mieuxvoter.fr Dans le but de décider collectivement En tant que collectif démocratique moderne From d8ebbba3eba8e57204f1f37ba17016ec90cd7305 Mon Sep 17 00:00:00 2001 From: domi41 Date: Thu, 30 Apr 2020 12:57:15 +0200 Subject: [PATCH 40/53] feat(features): use localized hamcrest branch We hacked (very) partial I18N support in hamcrest. Still need to figure out if we're going to use `gettext` or `python-i18n`, or even something else. --- requirements.txt | 3 ++- tests/acceptance/environment.py | 29 ++++++++++++++++++++++++---- tests/acceptance/steps/tools_i18n.py | 10 ++++++---- tests/acceptance/steps/tools_nlp.py | 4 ++-- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index a1b345c..76063da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,8 @@ nose==1.3.7 #behave-django==1.3.0 # Assertion library -pyhamcrest==2.0.2 +#pyhamcrest==2.0.2 +git+git://github.com/domi41/PyHamcrest@hack-i18n#egg=pyhamcrest # Test Metrics coverage>=4.5.1 diff --git a/tests/acceptance/environment.py b/tests/acceptance/environment.py index 27ac5ef..d0e25da 100644 --- a/tests/acceptance/environment.py +++ b/tests/acceptance/environment.py @@ -2,7 +2,7 @@ Environment module for acceptance testing of the scenaristic constitution. https://behave.readthedocs.io/en/latest/api.html#environment-file-functions """ - +from behave.log_capture import capture from steps.context_main import reset_context as reset_main_context @@ -11,6 +11,9 @@ # expressions (I18N, epicene) we make regular expressions the default. # This is at the expense of automatic type casting of step variables. from behave import use_step_matcher + +from steps.tools_i18n import guess_language + use_step_matcher("re") # You can still override this right before your step def if you want, # by calling use_step_matcher() with one of the following: @@ -18,11 +21,22 @@ # and calling it again with "re" after your step def. (it uses a `global`) +def before_all(context): + """ + Ran before the whole shooting match. + """ + pass + + def before_feature(context, feature): """ Ran before _each_ feature file is exercised. """ - pass + locale = guess_language(context) + # REQUIRES CUSTOM FORK OF HAMCREST + from hamcrest import set_locale + set_locale(locale) + ################################## def before_scenario(context, scenario): @@ -33,9 +47,16 @@ def before_scenario(context, scenario): pass -def before_all(context): +def after_scenario(context, scenario): """ - Ran before the whole shooting match. + Ran after _each_ scenario is run. + """ + pass + + +def after_step(context, step): + """ + Ran after _each_ step is run. """ pass diff --git a/tests/acceptance/steps/tools_i18n.py b/tests/acceptance/steps/tools_i18n.py index 2603cd0..cf3a8bb 100644 --- a/tests/acceptance/steps/tools_i18n.py +++ b/tests/acceptance/steps/tools_i18n.py @@ -209,9 +209,11 @@ ] -def guess_language(context): +# guess_locale()? +def guess_language(context): # naive, inefficient for language in languages: - if language[0] in context.tags: - return language[0] - raise Exception("No language found. Use a tag @en or @fr?") + for tag in context.tags: + if language[0] == tag[0:2]: + return tag # eg: "fr_FR" + raise Exception("No language found. Use a tag @en_US or @fr_FR?") diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index d2415e5..cb07258 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -16,7 +16,7 @@ def parse_amount(context, amount_string): :param string amount_string: :return int|float: """ - language = guess_language(context) + language = guess_language(context)[0:2] if 'fr' == language: if re.match("^aucun(?:[⋅.-]?e)?$", amount_string): return 0 @@ -40,7 +40,7 @@ def parse_yaml(context, with_i18n=True): language = guess_language(context) # Eventually, load these maps from files, perhaps yaml_keys_map = { - 'fr': { + 'fr_FR': { 'titre': 'title', 'candidats': 'candidates', 'candidates': 'candidates', From 85c44f2cca32363488b947218ae1fb4965bd375c Mon Sep 17 00:00:00 2001 From: domi41 Date: Thu, 30 Apr 2020 13:17:35 +0200 Subject: [PATCH 41/53] feat(features): set the locale as well Perhaps behave already does this behind the scenes. It does not affect the output of the built-in AssertionError. --- tests/acceptance/environment.py | 14 +++++++------- tests/acceptance/steps/tools_i18n.py | 3 +-- tests/acceptance/steps/tools_nlp.py | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/acceptance/environment.py b/tests/acceptance/environment.py index d0e25da..e6abb14 100644 --- a/tests/acceptance/environment.py +++ b/tests/acceptance/environment.py @@ -2,18 +2,17 @@ Environment module for acceptance testing of the scenaristic constitution. https://behave.readthedocs.io/en/latest/api.html#environment-file-functions """ -from behave.log_capture import capture + +from locale import setlocale, LC_TIME from steps.context_main import reset_context as reset_main_context +from steps.tools_i18n import guess_locale # Since we expect most of our step defs to require the flexibility of regular # expressions (I18N, epicene) we make regular expressions the default. # This is at the expense of automatic type casting of step variables. from behave import use_step_matcher - -from steps.tools_i18n import guess_language - use_step_matcher("re") # You can still override this right before your step def if you want, # by calling use_step_matcher() with one of the following: @@ -32,10 +31,11 @@ def before_feature(context, feature): """ Ran before _each_ feature file is exercised. """ - locale = guess_language(context) + context_locale = guess_locale(context) + setlocale(LC_TIME, "%s.UTF-8" % context_locale) # REQUIRES CUSTOM FORK OF HAMCREST - from hamcrest import set_locale - set_locale(locale) + from hamcrest import set_locale as set_hamcrest_locale + set_hamcrest_locale(context_locale) ################################## diff --git a/tests/acceptance/steps/tools_i18n.py b/tests/acceptance/steps/tools_i18n.py index cf3a8bb..a772a5f 100644 --- a/tests/acceptance/steps/tools_i18n.py +++ b/tests/acceptance/steps/tools_i18n.py @@ -209,8 +209,7 @@ ] -# guess_locale()? -def guess_language(context): # naive, inefficient +def guess_locale(context): # naive, inefficient for language in languages: for tag in context.tags: if language[0] == tag[0:2]: diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index cb07258..cd095dd 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -5,7 +5,7 @@ import re from text_to_num import text2num -from tools_i18n import guess_language +from tools_i18n import guess_locale def parse_amount(context, amount_string): @@ -16,7 +16,7 @@ def parse_amount(context, amount_string): :param string amount_string: :return int|float: """ - language = guess_language(context)[0:2] + language = guess_locale(context)[0:2] if 'fr' == language: if re.match("^aucun(?:[⋅.-]?e)?$", amount_string): return 0 @@ -37,7 +37,7 @@ def parse_yaml(context, with_i18n=True): data = safe_load(context.text) if with_i18n: - language = guess_language(context) + language = guess_locale(context) # Eventually, load these maps from files, perhaps yaml_keys_map = { 'fr_FR': { From c04a0f9dc9a9f7972320a6788f330c4de3ef7135 Mon Sep 17 00:00:00 2001 From: domi41 Date: Thu, 30 Apr 2020 16:59:28 +0200 Subject: [PATCH 42/53] feat(features): implement steps about voting through the REST API Eating a chartreuse cookie. --- .../fr_FR/00.deboguer-les-etapes.feature | 22 ++++++++++++- .../steps/steps_esoteric_scrutin.py | 17 ++++++---- tests/acceptance/steps/steps_rest_scrutin.py | 21 ++++++++++-- tests/acceptance/steps/toolbox.py | 13 ++++++-- tests/acceptance/steps/tools_dbal.py | 17 ++++++++-- tests/acceptance/steps/tools_nlp.py | 33 +++++++++++++++++++ 6 files changed, 109 insertions(+), 14 deletions(-) diff --git a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature index c6273e6..2debe46 100644 --- a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature @@ -51,7 +51,7 @@ Scénario: Compter les scrutins - Telegram - Telegram """ - Alors il devrait maintenant y avoir deux scrutins dans la base de données + Alors il devrait maintenant y avoir trente deux scrutins dans la base de données @@ -69,7 +69,27 @@ Scénario: Soumettre un nouveau scrutin +# This scenario is expected to fail as soon as we implement any form of security. +# When that happens, don't hesitate about deleting it. +@weak @new +Scénario: Voter sur un scrutin + Étant donné un scrutin comme suit: + """ + titre: La liberté de la presse + candidats: + - France + - Islande + """ + Et quelqu'un vote comme suit sur ce scrutin: + """ + France: insuffisant + Islande: très bien + """ + Alors il devrait maintenant y avoir un scrutin dans la base de données + + + Scénario: Afficher un citoyen Étant donné un citoyen nommé Michel Balinski Alors je débogue le citoyen nommé Michel Balinski diff --git a/tests/acceptance/steps/steps_esoteric_scrutin.py b/tests/acceptance/steps/steps_esoteric_scrutin.py index b902d73..2b190c9 100644 --- a/tests/acceptance/steps/steps_esoteric_scrutin.py +++ b/tests/acceptance/steps/steps_esoteric_scrutin.py @@ -5,6 +5,9 @@ from behave import given, when, then, step from hamcrest import assert_that, equal_to +from datetime import datetime as clock, timedelta + +from election.models import Election from tools_nlp import parse_amount, parse_yaml from tools_dbal import count_polls @@ -17,13 +20,13 @@ @step(u"un(?: autre)? scrutin comme suit *:?") def there_is_a_poll_like_so(context): data = parse_yaml(context) - from election.models import Election - election = Election() - election.title = data.get('title') - election.candidates = data.get('candidates') - election.num_grades = data.get('grades', 7) - election.save() - context.that_poll = election + poll = Election() + poll.title = data.get('title') + poll.candidates = data.get('candidates') + poll.num_grades = data.get('grades', 7) + poll.finish_at = (timedelta(hours=1) + clock.now()).timestamp() + poll.save() + context.that_poll = poll # @then diff --git a/tests/acceptance/steps/steps_rest_scrutin.py b/tests/acceptance/steps/steps_rest_scrutin.py index b0c48c6..70ccc13 100644 --- a/tests/acceptance/steps/steps_rest_scrutin.py +++ b/tests/acceptance/steps/steps_rest_scrutin.py @@ -6,7 +6,7 @@ from behave import given, when, then, step from hamcrest import assert_that, equal_to -from toolbox import parse_actor, parse_yaml +from toolbox import parse_actor, parse_yaml, parse_grades, find_poll ############################################################################### @@ -17,8 +17,25 @@ def actor_creates_a_poll_like_so(context, actor): data = parse_yaml(context) actor = parse_actor(context, actor) + title = data.get('title') actor.post('/polls', data={ - 'title': data.get('title'), + 'title': title, 'candidates': data.get('candidates'), 'num_grades': data.get('grades', 7), }) + created_poll = find_poll(title, relax=True) + if created_poll: + context.that_poll = created_poll + + +# @when +@step(u"(?P.+) vote comme suit sur ce scrutin *:?") +def actor_votes_on_that_poll_like_so(context, actor): + actor = parse_actor(context, actor) + data = parse_yaml(context) + poll = context.that_poll + grades = parse_grades(context, data, poll) + actor.post("/votes", data={ + 'election': poll.id, + 'grades_by_candidate': grades, + }) diff --git a/tests/acceptance/steps/toolbox.py b/tests/acceptance/steps/toolbox.py index f2d2374..9ec855a 100644 --- a/tests/acceptance/steps/toolbox.py +++ b/tests/acceptance/steps/toolbox.py @@ -5,15 +5,22 @@ from django.test import Client + +# Keep these, it is used by those who import toolbox +from tools_dbal import * from tools_nlp import * class Actor(object): + """ + Light wrapper around HTTP clients, where we can put our nitty-gritty. + """ def __init__(self, name=None) -> None: super().__init__() self.name = name self.client = Client() + self.last_response = None def adjust_path(self, path): if path.startswith('/'): @@ -21,15 +28,17 @@ def adjust_path(self, path): return "/api/election/%s" % path def handle_possible_failure(self, method, path, response): + # FIXME: I18N if response.status_code >= 400: - print("%s %s (%d)" % (method, path, response.status_code)) + print("%s %s (%d)\n" % (method, path, response.status_code)) print(response.content) + raise AssertionError("Request should succeed.") return response def post(self, path, data, safe_to_fail=False): path = self.adjust_path(path) response = self.client.post(path=path, data=data) - # response = self.client.generic(path=path, method="POST", data=data) + self.last_response = response if not safe_to_fail: self.handle_possible_failure('POST', path, response) diff --git a/tests/acceptance/steps/tools_dbal.py b/tests/acceptance/steps/tools_dbal.py index a976080..323db2f 100644 --- a/tests/acceptance/steps/tools_dbal.py +++ b/tests/acceptance/steps/tools_dbal.py @@ -23,8 +23,21 @@ def count_polls(): # TBD: "scrutin" translates to "poll"? return Election.objects.count() -def find_user(identifier): +def find_user(identifier, relax=False): user = User.objects.get(username=identifier) if user is not None: return user - raise ValueError("No user found matching `%s`." % identifier) + if not relax: + raise ValueError("No user found matching `%s`." % identifier) + return None + + +def find_poll(identifier, relax=False): + poll = Election.objects.get(title=identifier) + if poll is not None: + return poll + if not relax: + raise ValueError("No poll found matching `%s`." % identifier) + return None + + diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index cd095dd..7ac7c79 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -54,3 +54,36 @@ def parse_yaml(context, with_i18n=True): data[yaml_keys_map[language][key]] = data[key] return data + + +def parse_grades(context, data, poll): + grades = [] + + for candidate in poll.candidates: + if candidate not in data: + raise AssertionError("Candidate `%s' not found in `%s'." % (candidate, data)) + grade_text = data[candidate] + + grades_database = { # TBD + 'fr_FR': { + 'quality': { + '7': [ + ['excellent', 'excellente', 'excellent⋅e', 'excellent-e', 'excellent.e'], + ['très bien'], + ['bien'], + ['assez bien'], + ['passable'], + ['insuffisant', 'insuffisante', 'insuffisant⋅e', 'insuffisant-e', 'insuffisant.e'], + ['à rejeter'], + ], + }, + } + } + locale = guess_locale(context) + grades_in_order = grades_database[locale]['quality']['7'] + for k, matching_grades in enumerate(grades_in_order): + if grade_text in matching_grades: + grades.append(k) + break + + return grades From 9aa7cb1800d118c61721fa26079921d830f35e6e Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 1 May 2020 19:03:09 +0200 Subject: [PATCH 43/53] docs(features) --- .../fr_FR/00.deboguer-les-etapes.feature | 21 +++++++++---------- tests/acceptance/steps/steps_ouputting.py | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature index 2debe46..d789ab3 100644 --- a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature @@ -1,4 +1,6 @@ #language: fr +# These scenario are expected to fail as soon as we implement any form of security. +# When that happens, don't hesitate about removing them, their purpose will be outlived. @fr_FR @vigil Fonctionnalité: Décrire les comportements des étapes des scénarios @@ -60,26 +62,23 @@ Scénario: Soumettre un nouveau scrutin """ titre: Les Histoires Canines candidats: - - Milou - - Laika - - Cerbère - - Lassie + - Milou + - Laika + - Cerbère + - Lassie """ Alors il devrait maintenant y avoir un scrutin dans la base de données -# This scenario is expected to fail as soon as we implement any form of security. -# When that happens, don't hesitate about deleting it. @weak -@new Scénario: Voter sur un scrutin Étant donné un scrutin comme suit: """ titre: La liberté de la presse candidats: - - France - - Islande + - France + - Islande """ Et quelqu'un vote comme suit sur ce scrutin: """ @@ -91,5 +90,5 @@ Scénario: Voter sur un scrutin Scénario: Afficher un citoyen - Étant donné un citoyen nommé Michel Balinski - Alors je débogue le citoyen nommé Michel Balinski + Étant donné un citoyen nommé Rida Laraki + Alors j'affiche le citoyen nommé Rida Laraki diff --git a/tests/acceptance/steps/steps_ouputting.py b/tests/acceptance/steps/steps_ouputting.py index 7adc9b3..c241508 100644 --- a/tests/acceptance/steps/steps_ouputting.py +++ b/tests/acceptance/steps/steps_ouputting.py @@ -7,8 +7,8 @@ from tools_dbal import find_user -@step(u"j(?:e |')(?:débogue|affiche)(?: l[ea])? citoyen(?:[⋅.-]?ne|)(?: nommé(?:[⋅.-]?e|))? (?P.+)") -@step(u"I print(?: the)? user(?: named)? (?P.+)") +@step(u"(?:j(?:e |'))?(?:débogue|affiche)(?: l[ea])? citoyen(?:[⋅.-]?ne|)(?: nommé(?:[⋅.-]?e|))? (?P.+)") +@step(u"(?:I )?print(?: the)? user(?: named)? (?P.+)") def print_user(context, name): user = find_user(name) print("%s\n" % user) From 37567f9df518d88c5e4752803224f5595113d406 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 2 May 2020 09:10:14 +0200 Subject: [PATCH 44/53] fix(features): invert grades order, so that 0 == worst --- tests/acceptance/steps/tools_nlp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index 7ac7c79..9b1393d 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -68,13 +68,13 @@ def parse_grades(context, data, poll): 'fr_FR': { 'quality': { '7': [ - ['excellent', 'excellente', 'excellent⋅e', 'excellent-e', 'excellent.e'], - ['très bien'], - ['bien'], - ['assez bien'], - ['passable'], - ['insuffisant', 'insuffisante', 'insuffisant⋅e', 'insuffisant-e', 'insuffisant.e'], ['à rejeter'], + ['insuffisant', 'insuffisante', 'insuffisant⋅e', 'insuffisant-e', 'insuffisant.e'], + ['passable'], + ['assez bien'], + ['bien'], + ['très bien'], + ['excellent', 'excellente', 'excellent⋅e', 'excellent-e', 'excellent.e'], ], }, } From c1a1f7b597666d96fe2b90bda10b0848df78fb4f Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 2 May 2020 14:59:11 +0200 Subject: [PATCH 45/53] feat(features): prepare fetching poll results --- .../features/fr_FR/00.deboguer-les-etapes.feature | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature index d789ab3..19f7535 100644 --- a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature @@ -85,7 +85,13 @@ Scénario: Voter sur un scrutin France: insuffisant Islande: très bien """ + Et quelqu'un d'autre juge les candidats de ce scrutin comme suit: + """ + France: passable + Islande: excellent + """ Alors il devrait maintenant y avoir un scrutin dans la base de données + Et le vainqueur de ce scrutin devrait être: France From 0ecdb5c0f19d10b04bb7757e01751b2a65328ee5 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 2 May 2020 17:05:13 +0200 Subject: [PATCH 46/53] chore(PEP8) --- election/serializers.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 election/serializers.py diff --git a/election/serializers.py b/election/serializers.py new file mode 100644 index 0000000..e69de29 From 1a44e70d5ad4aced6b86d262ed821875000ca36f Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 2 May 2020 17:18:45 +0200 Subject: [PATCH 47/53] feat(features): fetch poll results We also introduce a step to pass the time. --- .../fr_FR/00.deboguer-les-etapes.feature | 2 +- .../steps/steps_esoteric_scrutin.py | 2 +- tests/acceptance/steps/steps_rest_scrutin.py | 29 ++++++++++++++++--- tests/acceptance/steps/steps_time.py | 15 ++++++++++ tests/acceptance/steps/toolbox.py | 14 +++++++++ tests/acceptance/steps/tools_nlp.py | 1 + 6 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 tests/acceptance/steps/steps_time.py diff --git a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature index 19f7535..8c5b7ed 100644 --- a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature +++ b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature @@ -91,7 +91,7 @@ Scénario: Voter sur un scrutin Islande: excellent """ Alors il devrait maintenant y avoir un scrutin dans la base de données - Et le vainqueur de ce scrutin devrait être: France + Et le vainqueur de ce scrutin devrait être: Islande diff --git a/tests/acceptance/steps/steps_esoteric_scrutin.py b/tests/acceptance/steps/steps_esoteric_scrutin.py index 2b190c9..a479f7b 100644 --- a/tests/acceptance/steps/steps_esoteric_scrutin.py +++ b/tests/acceptance/steps/steps_esoteric_scrutin.py @@ -24,7 +24,7 @@ def there_is_a_poll_like_so(context): poll.title = data.get('title') poll.candidates = data.get('candidates') poll.num_grades = data.get('grades', 7) - poll.finish_at = (timedelta(hours=1) + clock.now()).timestamp() + poll.finish_at = (timedelta(seconds=data.get('duration', 3600)) + clock.now()).timestamp() poll.save() context.that_poll = poll diff --git a/tests/acceptance/steps/steps_rest_scrutin.py b/tests/acceptance/steps/steps_rest_scrutin.py index 70ccc13..90f4d69 100644 --- a/tests/acceptance/steps/steps_rest_scrutin.py +++ b/tests/acceptance/steps/steps_rest_scrutin.py @@ -6,7 +6,7 @@ from behave import given, when, then, step from hamcrest import assert_that, equal_to -from toolbox import parse_actor, parse_yaml, parse_grades, find_poll +from toolbox import parse_actor, parse_yaml, parse_grades, find_poll, fail ############################################################################### @@ -29,13 +29,34 @@ def actor_creates_a_poll_like_so(context, actor): # @when -@step(u"(?P.+) vote comme suit sur ce scrutin *:?") -def actor_votes_on_that_poll_like_so(context, actor): +@step(u"(?P.+) (?:vote|juge les candidats)(?: comme suit)? (?:sur|de) ce scrutin(?: comme suit)? *:?") +def actor_judges_candidates_of_that_poll_like_so(context, actor): actor = parse_actor(context, actor) data = parse_yaml(context) poll = context.that_poll grades = parse_grades(context, data, poll) - actor.post("/votes", data={ + actor.post("/judgments", data={ 'election': poll.id, 'grades_by_candidate': grades, }) + + +# @when +@step(u"l[ea] vainqueur(?:[⋅.-]?e)? de ce scrutin devrait être: (?P.+)") +def winner_of_that_poll_should_be(context, candidate): + actor = parse_actor(context, "C0h4N") + poll = context.that_poll + response = actor.get(f"/results/{poll.id}/") + # from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect + # if HttpResponsePermanentRedirect.status_code == response.status_code \ + # or HttpResponseRedirect.status_code == response.status_code: + # print(f"Response: `{response}'\n") + # fail("You cannot check the winners of this poll yet. Wait until it closes.") + + # data example : + # [{'name': 'Islande', 'id': 1, + # 'profile': {'0': 0, '1': 0, '2': 0, '3': 0, '4': 0, '5': 1, '6': 1}, 'grade': 5}, + # {'name': 'France', 'id': 0, + # 'profile': {'0': 0, '1': 1, '2': 1, '3': 0, '4': 0, '5': 0, '6': 0}, 'grade': 1}] + data = response.json() + assert_that(data[0]['name'], equal_to(candidate)) diff --git a/tests/acceptance/steps/steps_time.py b/tests/acceptance/steps/steps_time.py new file mode 100644 index 0000000..2880354 --- /dev/null +++ b/tests/acceptance/steps/steps_time.py @@ -0,0 +1,15 @@ +""" +Basic control of time. +""" + +from behave import step +from time import sleep + + +from tools_nlp import parse_amount + + +@step(u"j'attends (?P.+) secondes") +def i_wait_for_seconds(context, amount): + amount = parse_amount(context, amount) + sleep(amount) diff --git a/tests/acceptance/steps/toolbox.py b/tests/acceptance/steps/toolbox.py index 9ec855a..3b237dd 100644 --- a/tests/acceptance/steps/toolbox.py +++ b/tests/acceptance/steps/toolbox.py @@ -35,6 +35,15 @@ def handle_possible_failure(self, method, path, response): raise AssertionError("Request should succeed.") return response + def get(self, path, data=None, safe_to_fail=False): + path = self.adjust_path(path) + response = self.client.get(path=path, data=data) + self.last_response = response + + if not safe_to_fail: + self.handle_possible_failure('GET', path, response) + return response + def post(self, path, data, safe_to_fail=False): path = self.adjust_path(path) response = self.client.post(path=path, data=data) @@ -42,6 +51,7 @@ def post(self, path, data, safe_to_fail=False): if not safe_to_fail: self.handle_possible_failure('POST', path, response) + return response def parse_actor(context, actor_name): @@ -50,3 +60,7 @@ def parse_actor(context, actor_name): if actor_name not in context.actors: context.actors[actor_name] = Actor(name=actor_name) return context.actors[actor_name] + + +def fail(message): # TBD; How to I18N AssertionError? + raise AssertionError(message) \ No newline at end of file diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index 9b1393d..6c1f3d9 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -45,6 +45,7 @@ def parse_yaml(context, with_i18n=True): 'candidats': 'candidates', 'candidates': 'candidates', 'candidat⋅es': 'candidates', + 'durée': 'duration', }, } # Naive mapping, not collision-resilient, but it's ok for now From 9a76225329a4800c4574fd4b95e22fb027230a81 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 2 May 2020 17:24:47 +0200 Subject: [PATCH 48/53] feat(urls): create a RESTful alias for fetching poll results --- tests/acceptance/steps/steps_rest_scrutin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/acceptance/steps/steps_rest_scrutin.py b/tests/acceptance/steps/steps_rest_scrutin.py index 90f4d69..a08623f 100644 --- a/tests/acceptance/steps/steps_rest_scrutin.py +++ b/tests/acceptance/steps/steps_rest_scrutin.py @@ -46,12 +46,8 @@ def actor_judges_candidates_of_that_poll_like_so(context, actor): def winner_of_that_poll_should_be(context, candidate): actor = parse_actor(context, "C0h4N") poll = context.that_poll - response = actor.get(f"/results/{poll.id}/") - # from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect - # if HttpResponsePermanentRedirect.status_code == response.status_code \ - # or HttpResponseRedirect.status_code == response.status_code: - # print(f"Response: `{response}'\n") - # fail("You cannot check the winners of this poll yet. Wait until it closes.") + # "/results/{poll.id}/" + response = actor.get(f"/polls/{poll.id}/results") # data example : # [{'name': 'Islande', 'id': 1, From 0586fe15b8aaefb35d47b4e100d9e75efc566da8 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 2 May 2020 19:50:50 +0200 Subject: [PATCH 49/53] chore(features) --- tests/acceptance/steps/steps_rest_scrutin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/acceptance/steps/steps_rest_scrutin.py b/tests/acceptance/steps/steps_rest_scrutin.py index a08623f..fdaa611 100644 --- a/tests/acceptance/steps/steps_rest_scrutin.py +++ b/tests/acceptance/steps/steps_rest_scrutin.py @@ -49,10 +49,10 @@ def winner_of_that_poll_should_be(context, candidate): # "/results/{poll.id}/" response = actor.get(f"/polls/{poll.id}/results") - # data example : - # [{'name': 'Islande', 'id': 1, - # 'profile': {'0': 0, '1': 0, '2': 0, '3': 0, '4': 0, '5': 1, '6': 1}, 'grade': 5}, - # {'name': 'France', 'id': 0, - # 'profile': {'0': 0, '1': 1, '2': 1, '3': 0, '4': 0, '5': 0, '6': 0}, 'grade': 1}] data = response.json() + # data example : + # [{'name': 'Islande', 'id': 1, 'grade': 5, + # 'profile': {'0': 0, '1': 0, '2': 0, '3': 0, '4': 0, '5': 1, '6': 1}}, + # {'name': 'France', 'id': 0, 'grade': 1, + # 'profile': {'0': 0, '1': 1, '2': 1, '3': 0, '4': 0, '5': 0, '6': 0}}] assert_that(data[0]['name'], equal_to(candidate)) From 4c26741379e79dafabd344cc2718cdbe257467eb Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 16 May 2020 05:48:20 +0200 Subject: [PATCH 50/53] Move the features out of the project. We'll move them into a submodule or a subtree? --- .../fr_FR/00.deboguer-les-etapes.feature | 100 ------------------ .../fr_FR/10.creer-un-scrutin.feature | 43 -------- 2 files changed, 143 deletions(-) delete mode 100644 tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature delete mode 100644 tests/acceptance/features/fr_FR/10.creer-un-scrutin.feature diff --git a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature b/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature deleted file mode 100644 index 8c5b7ed..0000000 --- a/tests/acceptance/features/fr_FR/00.deboguer-les-etapes.feature +++ /dev/null @@ -1,100 +0,0 @@ -#language: fr -# These scenario are expected to fail as soon as we implement any form of security. -# When that happens, don't hesitate about removing them, their purpose will be outlived. -@fr_FR -@vigil -Fonctionnalité: Décrire les comportements des étapes des scénarios - Dans le but d'expliciter le comportement des étapes des scénarios - En tant que débogueur⋅es - Nous souhaitons les utiliser dans différents contextes - - - -Scénario: Requérir la présence à priori de citoyen⋅nes - # New keyword Sachant has not yet propagated to our behave version -# Sachant qu'il ne devrait y avoir aucun citoyen dans la base de données - # So we use the less idiomatic equivalent - Étant donné qu'il ne devrait y avoir aucun citoyen dans la base de données - Étant donné un citoyen nommé Michel Balinski - Alors il devrait y avoir un citoyen dans la base de données - - - -Scénario: Requérir la présence à priori de scrutins - Sachant qu'il ne devrait y avoir aucun scrutin dans la base de données - Étant donné un scrutin comme suit: - """ - titre: Responsable de l'animation du chantier Constituance Algorithmique - candidats: - - Pierre-Louis Guhur - - Chloé Ridel - - Dominique Merle - """ - Alors il devrait y avoir un scrutin dans la base de données - - - -Scénario: Compter les scrutins - Sachant qu'il ne devrait y avoir aucun scrutin dans la base de données - Étant donné un scrutin comme suit: - """ - titre: Application JM préférée - candidats: - - app.mieuxvoter.fr - - jugementmajoritaire.net - - lechoixcommun.fr - """ - Alors il devrait y avoir un scrutin dans la base de données - Mais ce n'est pas tout ! - Étant donné un autre scrutin comme suit: - """ - titre: Canal de communication interne - candidats: - - Telegram - - Telegram - """ - Alors il devrait maintenant y avoir trente deux scrutins dans la base de données - - - -Scénario: Soumettre un nouveau scrutin - Quand quelqu'un crée un scrutin comme suit: - """ - titre: Les Histoires Canines - candidats: - - Milou - - Laika - - Cerbère - - Lassie - """ - Alors il devrait maintenant y avoir un scrutin dans la base de données - - - -@weak -Scénario: Voter sur un scrutin - Étant donné un scrutin comme suit: - """ - titre: La liberté de la presse - candidats: - - France - - Islande - """ - Et quelqu'un vote comme suit sur ce scrutin: - """ - France: insuffisant - Islande: très bien - """ - Et quelqu'un d'autre juge les candidats de ce scrutin comme suit: - """ - France: passable - Islande: excellent - """ - Alors il devrait maintenant y avoir un scrutin dans la base de données - Et le vainqueur de ce scrutin devrait être: Islande - - - -Scénario: Afficher un citoyen - Étant donné un citoyen nommé Rida Laraki - Alors j'affiche le citoyen nommé Rida Laraki diff --git a/tests/acceptance/features/fr_FR/10.creer-un-scrutin.feature b/tests/acceptance/features/fr_FR/10.creer-un-scrutin.feature deleted file mode 100644 index 12b45f3..0000000 --- a/tests/acceptance/features/fr_FR/10.creer-un-scrutin.feature +++ /dev/null @@ -1,43 +0,0 @@ -#language: fr -@fr_FR -Fonctionnalité: Créer un scrutin au jugement majoritaire sur app.mieuxvoter.fr - Dans le but de décider collectivement - En tant que collectif démocratique moderne - Nous souhaitons créer un scrutin au jugement majoritaire - - # Écrivez d'autres intentions, si vous le souhaitez -# Dans le but de ? -# En tant que ? -# Nous|Je ? - - -Contexte: - Étant donné un citoyen nommé Michel Balinski - Et un citoyen nommé Rida Laraki - Et une citoyenne nommée Maria Balinska - # … - - -@wip -Scénario: Créer un scrutin au jugement majoritaire - Quand ??? crée un scrutin comme suit: - """ - titre: ??? - candidats: - - ??? - - ??? - - ??? - ???: ??? - """ - Et ??? vote comme suit sur ce scrutin: - """ - ???: ??? - ???: ??? - ???: ??? - """ - Et … - Alors ??? doit être le candidat élu de ce scrutin - # … - # @all: Avez-vous des exemples de scrutins ? - # N'hésitez pas à vous aussi écrire ce scénario dans votre branche, - # si besoin on pourra délibérer au JM dessus ;) From f107b237fd38210aa2d0515c765de3be24529bfe Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 16 May 2020 05:50:08 +0200 Subject: [PATCH 51/53] Ignore virtualenv files. --- tests/acceptance/features/fr_FR | 1 + 1 file changed, 1 insertion(+) create mode 160000 tests/acceptance/features/fr_FR diff --git a/tests/acceptance/features/fr_FR b/tests/acceptance/features/fr_FR new file mode 160000 index 0000000..91486c9 --- /dev/null +++ b/tests/acceptance/features/fr_FR @@ -0,0 +1 @@ +Subproject commit 91486c99175021af6c595043b59ed56ca5f96632 From 9933fc60e7384bd63b96da4882e4c9028f173191 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sun, 17 May 2020 03:51:31 +0200 Subject: [PATCH 52/53] fix(behave): fix the mangling of error messages of the pretty formatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … I don't understand why this bug has been alive for years. The Formatter interface is well-thought, thankfully. --- .behaverc | 15 +++++-- tests/__init__.py | 0 tests/acceptance/__init__.py | 0 tests/acceptance/formatter.py | 80 +++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/acceptance/__init__.py create mode 100644 tests/acceptance/formatter.py diff --git a/.behaverc b/.behaverc index 6885a58..a23bd87 100644 --- a/.behaverc +++ b/.behaverc @@ -2,7 +2,6 @@ ; We are not using the default path to leave room for unit tests ; also, apps. paths = tests/acceptance - # What is going on? Does this change anything for you? --format=plain works ;format = plain @@ -11,10 +10,18 @@ paths = tests/acceptance ;logging_filter = -suds ; Show all print() statements even if tests pass. (bugs! bugs!) -;stderr_capture = False +stderr_capture = False stdout_capture = False -;log_capture = False +log_capture = False + +show_timings = False +show_source = False +show_multiline = True show_skipped = False show_snippets = False -summary = True \ No newline at end of file +summary = True + +[behave.formatters] +# We override the buggy "pretty" formatter +pretty = tests.acceptance.formatter:BeautifulFormatter \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/__init__.py b/tests/acceptance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/formatter.py b/tests/acceptance/formatter.py new file mode 100644 index 0000000..9d8fd4d --- /dev/null +++ b/tests/acceptance/formatter.py @@ -0,0 +1,80 @@ +import six +from behave.formatter.pretty import PrettyFormatter +from behave.formatter.base import Formatter +from behave.textutil import indent + + +class BeautifulFormatter(PrettyFormatter): + """ + Quick patch for the most annoying bug of the PrettyFormatter. + Eventually we should move to our own CustomFormatter, + so that captured logs and stdout are printed AFTER the step, + like in high-quality gherkin runners in other languages. + """ + def result(self, step): + if not self.monochrome: + lines = self.step_lines + 1 + if self.show_multiline: + if step.table: + lines += len(step.table.rows) + 1 + if step.text: + lines += len(step.text.splitlines()) + 2 + # self.stream.write(up(lines)) # NO GOD PLEASE NO + arguments = [] + location = None + if self._match: + arguments = self._match.arguments + location = self._match.location + self.print_step(step.status, arguments, location, True) + if step.error_message: + self.stream.write(indent(step.error_message.strip(), u" ")) + self.stream.write("\n\n") + self.stream.flush() + + def match(self, match): + self._match = match + self.print_statement() + self.stream.flush() + + +class CustomFormatter(Formatter): + """ + Half-assed attempt at making our own formatter entirely. + We don't use this for now, we're using the BeautifulFormatter above. + """ + + # indentation = "\t" + indentation = " " + + def indent(self, text): + p = self.indentation + lines = text.split("\n") + return "\n".join(["%s%s" % ("" if "" == line else p, line) for line in lines]) + + def feature(self, feature): + self.stream.write(u"\n") + self.stream.write(u"%s: %s\n" % ( + feature.keyword, + feature.name, + )) + for line in feature.description: + self.stream.write(self.indent(u"%s\n" % line)) + self.stream.flush() + + def scenario(self, scenario): + self.stream.write(u"\n") + self.stream.write(u"%s: %s\n" % ( + scenario.keyword, + scenario.name, + )) + self.stream.flush() + + def result(self, step): + self.stream.write(self.indent(u"%s %s\n" % ( + step.keyword, + step.name, + ))) + if step.error_message: + self.stream.write(self.indent(step.error_message.strip())) + self.stream.write("\n") + self.stream.flush() From 6150f61ed5da0aa7511d666f0f03e26f64395988 Mon Sep 17 00:00:00 2001 From: domi41 Date: Sat, 31 May 2025 02:59:29 +0200 Subject: [PATCH 53/53] chore: update to fastapi never mind, it can't be done properly for now dropping this --- INSTALL.md | 1 + README.md | 1 - app/crud.py | 4 +- app/database.py | 8 ++- app/main.py | 2 +- app/settings.py | 5 +- election/models.py | 0 election/serializers.py | 0 election/urls.py | 0 election/views.py | 0 requirements-dev.txt | 21 +++++++- requirements.txt | 20 +------ tests/acceptance/environment.py | 7 +-- tests/acceptance/steps/context_main.py | 1 + .../steps/steps_esoteric_scrutin.py | 35 ++++++++++--- tests/acceptance/steps/steps_esoteric_user.py | 2 +- tests/acceptance/steps/toolbox.py | 7 +-- tests/acceptance/steps/tools_dbal.py | 52 +++++++++++-------- tests/acceptance/steps/tools_nlp.py | 7 ++- 19 files changed, 108 insertions(+), 65 deletions(-) delete mode 100644 election/models.py delete mode 100644 election/serializers.py delete mode 100644 election/urls.py delete mode 100644 election/views.py diff --git a/INSTALL.md b/INSTALL.md index c549a7f..8a3b386 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,3 +1,4 @@ +# Install ## Install using Docker diff --git a/README.md b/README.md index fe5049c..dd67d6a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ You certainly want to apply the database migrations with: ./docker/migrate.sh - ## Run the tests ./docker/test.sh diff --git a/app/crud.py b/app/crud.py index ce674b5..24653a7 100644 --- a/app/crud.py +++ b/app/crud.py @@ -227,7 +227,7 @@ def _check_if_ref_exists(db: Session, ref: str): def create_election( - db: Session, election: schemas.ElectionCreate + db: Session, election: schemas.ElectionCreate, ) -> schemas.ElectionGet: # We create first the election # without candidates and grades @@ -236,7 +236,7 @@ def create_election( raise errors.InconsistentDatabaseError("Can not create election") election_ref = str(db_election.ref) - # Then, we add separatly candidates and grades + # Then, we add separately candidates and grades for candidate in election.candidates: params = candidate.model_dump() candidate = schemas.CandidateCreate(**params) diff --git a/app/database.py b/app/database.py index 3fb85eb..fbba13d 100644 --- a/app/database.py +++ b/app/database.py @@ -1,7 +1,9 @@ from __future__ import annotations + +from contextvars import ContextVar from urllib.parse import quote from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, declarative_base +from sqlalchemy.orm import sessionmaker, declarative_base, Session from .settings import settings @@ -19,7 +21,7 @@ engine = create_engine(database_url) SessionLocal: sessionmaker = sessionmaker( # type: ignore - autocommit=False, autoflush=False, bind=engine + autocommit=False, autoflush=False, bind=engine, ) Base = declarative_base() @@ -31,3 +33,5 @@ def get_db(): yield db finally: db.close() + +db_session: ContextVar[Session] = ContextVar('db_session') diff --git a/app/main.py b/app/main.py index 61b62df..19c5c48 100644 --- a/app/main.py +++ b/app/main.py @@ -77,7 +77,7 @@ async def inconsistent_database_exception_handler( return JSONResponse( status_code=500, content={ - "message": f"A serious error has occured with {exc.name}. {exc.details or ''}" + "message": f"A serious error has occurred with {exc.name}. {exc.details or ''}" }, ) diff --git a/app/settings.py b/app/settings.py index 0c247e5..3b75172 100644 --- a/app/settings.py +++ b/app/settings.py @@ -3,7 +3,10 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file=".env", extra="ignore") + model_config = SettingsConfigDict( + env_file=".env.local", + extra="ignore", + ) sqlite: bool = False diff --git a/election/models.py b/election/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/election/serializers.py b/election/serializers.py deleted file mode 100644 index e69de29..0000000 diff --git a/election/urls.py b/election/urls.py deleted file mode 100644 index e69de29..0000000 diff --git a/election/views.py b/election/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/requirements-dev.txt b/requirements-dev.txt index 3963e29..e2d5622 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,23 @@ alembic==1.8.1 types-python-jose==3.3.4 types-python-dateutil==2.8.2 mypy==1.15.0 -httpx>=0.22.0 \ No newline at end of file +httpx>=0.22.0 + +# Test Runners +#nose==1.3.7 +behave==1.2.6 +#django-nose==1.4.6 +#behave-django==1.3.0 + +# Assertion library +pyhamcrest==2.0.2 +#git+git://github.com/domi41/PyHamcrest@hack-i18n#egg=pyhamcrest + +# Test Metrics +coverage>=4.5.1 + +# Natural Language Processor for numbers +text2num>=2.2.0 + +# Markup language for humans +#PyYAML==5.3.1 diff --git a/requirements.txt b/requirements.txt index 76063da..16846d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,24 +4,6 @@ sqlalchemy==2.0.40 pydantic==2.11.3 psycopg2==2.9.5 git+https://github.com/MieuxVoter/majority-judgment-library-python -python-jose==3.3.0 +python-jose==3.4.0 python-dateutil==2.8.2 pydantic-settings==2.9.1 - -# Test Runners -nose==1.3.7 -#django-nose==1.4.6 -#behave-django==1.3.0 - -# Assertion library -#pyhamcrest==2.0.2 -git+git://github.com/domi41/PyHamcrest@hack-i18n#egg=pyhamcrest - -# Test Metrics -coverage>=4.5.1 - -# Natural Language Processor for numbers -text2num>=2.2.0 - -# Markup language for humans -PyYAML==5.3.1 diff --git a/tests/acceptance/environment.py b/tests/acceptance/environment.py index e6abb14..1a52b6c 100644 --- a/tests/acceptance/environment.py +++ b/tests/acceptance/environment.py @@ -3,6 +3,7 @@ https://behave.readthedocs.io/en/latest/api.html#environment-file-functions """ +from behave.runner import Context from locale import setlocale, LC_TIME from steps.context_main import reset_context as reset_main_context @@ -20,7 +21,7 @@ # and calling it again with "re" after your step def. (it uses a `global`) -def before_all(context): +def before_all(context: Context): """ Ran before the whole shooting match. """ @@ -34,8 +35,8 @@ def before_feature(context, feature): context_locale = guess_locale(context) setlocale(LC_TIME, "%s.UTF-8" % context_locale) # REQUIRES CUSTOM FORK OF HAMCREST - from hamcrest import set_locale as set_hamcrest_locale - set_hamcrest_locale(context_locale) + # from hamcrest import set_locale as set_hamcrest_locale + # set_hamcrest_locale(context_locale) ################################## diff --git a/tests/acceptance/steps/context_main.py b/tests/acceptance/steps/context_main.py index 229c5e5..f9f811c 100644 --- a/tests/acceptance/steps/context_main.py +++ b/tests/acceptance/steps/context_main.py @@ -12,5 +12,6 @@ def reset_context(context): + context.users = dict() context.that_user = None context.that_poll = None diff --git a/tests/acceptance/steps/steps_esoteric_scrutin.py b/tests/acceptance/steps/steps_esoteric_scrutin.py index a479f7b..f261ee1 100644 --- a/tests/acceptance/steps/steps_esoteric_scrutin.py +++ b/tests/acceptance/steps/steps_esoteric_scrutin.py @@ -4,28 +4,47 @@ """ from behave import given, when, then, step +from fastapi import Depends from hamcrest import assert_that, equal_to from datetime import datetime as clock, timedelta -from election.models import Election +from sqlalchemy.orm import Session +# from sqlalchemy.testing import db + +from app import schemas +from app.crud import create_election, create_candidate +# from app.database import get_db, db_session +from app.models import Grade +# from app.models import Election, Candidate from tools_nlp import parse_amount, parse_yaml from tools_dbal import count_polls -############################################################################### +# db: Session = Depends(get_db) # @given @step(u"un(?: autre)? scrutin comme suit *:?") def there_is_a_poll_like_so(context): data = parse_yaml(context) - poll = Election() - poll.title = data.get('title') - poll.candidates = data.get('candidates') - poll.num_grades = data.get('grades', 7) - poll.finish_at = (timedelta(seconds=data.get('duration', 3600)) + clock.now()).timestamp() - poll.save() + # db: Session = Depends(get_db) + poll = create_election( + db=db, + election=schemas.ElectionCreate( + name=data.get('title'), + candidates=data.get('candidates'), + grades=data.get('grades', [ + Grade(name="Excellent", description="", value=6), + Grade(name="Très Bien", description="", value=5), + Grade(name="Bien", description="", value=4), + Grade(name="Assez Bien", description="", value=3), + Grade(name="Passable", description="", value=2), + Grade(name="Insuffisant", description="", value=1), + Grade(name="À Rejeter", description="", value=0), + ]), + ), + ) context.that_poll = poll diff --git a/tests/acceptance/steps/steps_esoteric_user.py b/tests/acceptance/steps/steps_esoteric_user.py index 1a8206b..8dfdb38 100644 --- a/tests/acceptance/steps/steps_esoteric_user.py +++ b/tests/acceptance/steps/steps_esoteric_user.py @@ -32,4 +32,4 @@ def create_citizen_named(context, name): @step(u"(?:qu')?il(?: ne)? devrait y avoir (?P.+) citoyen(?:[⋅.-]?ne?|)s? dans la base de données") def there_should_be_n_users(context, amount): amount = parse_amount(context, amount) - assert_that(count_users(), equal_to(amount)) + assert_that(count_users(context), equal_to(amount)) diff --git a/tests/acceptance/steps/toolbox.py b/tests/acceptance/steps/toolbox.py index 3b237dd..b13ddf7 100644 --- a/tests/acceptance/steps/toolbox.py +++ b/tests/acceptance/steps/toolbox.py @@ -3,9 +3,10 @@ Not sure this is the python way. """ -from django.test import Client - +# from httpx import Client +from fastapi.testclient import TestClient as Client +from app.main import app # Keep these, it is used by those who import toolbox from tools_dbal import * from tools_nlp import * @@ -19,7 +20,7 @@ class Actor(object): def __init__(self, name=None) -> None: super().__init__() self.name = name - self.client = Client() + self.client = Client(app) self.last_response = None def adjust_path(self, path): diff --git a/tests/acceptance/steps/tools_dbal.py b/tests/acceptance/steps/tools_dbal.py index 323db2f..7c7adcf 100644 --- a/tests/acceptance/steps/tools_dbal.py +++ b/tests/acceptance/steps/tools_dbal.py @@ -1,43 +1,53 @@ """ Local database abstraction layer for step defs. """ +from behave.runner import Context +from fastapi import Depends +from sqlalchemy.orm import Session +# from sqlalchemy.testing import db +from app import errors +from app.crud import get_election +from app.database import get_db +from app.models import Election -from django.contrib.auth.models import User -from election.models import Election +db: Session = Depends(get_db) -def make_user(context, username): - return User.objects.create_user( - username=username, - email='user@test.mieuxvoter.fr', - password=username - ) +class User: + def __init__(self, username): + self.username = username -def count_users(): - return User.objects.count() +def make_user(context: Context, username): + user = User(username) + context.users[username] = user + return user -def count_polls(): # TBD: "scrutin" translates to "poll"? - return Election.objects.count() +def count_users(context: Context): + return len(context.users.items()) -def find_user(identifier, relax=False): - user = User.objects.get(username=identifier) + +def find_user(context: Context, username: str, relax=False): + user = context.users.get(username, None) if user is not None: return user if not relax: - raise ValueError("No user found matching `%s`." % identifier) + raise ValueError("No user found matching `%s`." % username) return None +def count_polls(): + return db.query(Election).count() + + def find_poll(identifier, relax=False): - poll = Election.objects.get(title=identifier) - if poll is not None: - return poll - if not relax: - raise ValueError("No poll found matching `%s`." % identifier) + try: + return get_election(db, identifier) + except errors.NotFoundError: + if not relax: + raise ValueError("No poll found matching `%s`." % identifier) return None - diff --git a/tests/acceptance/steps/tools_nlp.py b/tests/acceptance/steps/tools_nlp.py index 6c1f3d9..1e6e469 100644 --- a/tests/acceptance/steps/tools_nlp.py +++ b/tests/acceptance/steps/tools_nlp.py @@ -3,12 +3,14 @@ """ import re + +from behave.runner import Context from text_to_num import text2num from tools_i18n import guess_locale -def parse_amount(context, amount_string): +def parse_amount(context: Context, amount_string): """ - Multilingual (hopefully) EN - FR - … - Tailored for Gherkin features and behave @@ -24,7 +26,8 @@ def parse_amount(context, amount_string): if re.match("^no(?:ne)?$", amount_string): return 0 - return text2num(text=amount_string, lang=language, relaxed=True) + # return text2num(text=amount_string, lang=language, relaxed=True) + return text2num(text=amount_string, lang=language) def parse_yaml(context, with_i18n=True):