diff --git a/admin/files/nginx/https.conf.template b/admin/files/nginx/https.conf.template index 826207aaf..4e370d240 100644 --- a/admin/files/nginx/https.conf.template +++ b/admin/files/nginx/https.conf.template @@ -81,12 +81,6 @@ server { add_header "Pragma" "no-cache"; } - location /repondeur/static/ { - alias /srv/repondeur/src/repondeur/zam_repondeur/static/; - expires 30d; - add_header "Cache-Control" "public"; - } - location /_stats/ { alias /var/cache/munin/www/; add_header "Cache-Control" "no-store"; diff --git a/admin/tasks/system.py b/admin/tasks/system.py index 3c3618c2d..e50ad569a 100644 --- a/admin/tasks/system.py +++ b/admin/tasks/system.py @@ -181,7 +181,7 @@ def setup_self_signed_cert(ctx): " -days 365" " -sha256" " -nodes" - f" -subj '/C=FR/OU=Zam/CN={hostname}'" + f" -subj '/C=FR/OU=Visam/CN={hostname}'" ) diff --git a/repondeur/db_migrations/env.py b/repondeur/db_migrations/env.py index f5e9c2703..16d6ba1ed 100644 --- a/repondeur/db_migrations/env.py +++ b/repondeur/db_migrations/env.py @@ -3,6 +3,9 @@ from alembic import context from sqlalchemy import engine_from_config, pool +# Make sure all models are loaded +import zam_repondeur.models # noqa +import zam_repondeur.visam.models # noqa from zam_repondeur.models import Base # this is the Alembic Config object, which provides diff --git a/repondeur/db_migrations/versions/08e2b571d519_add_conseils_team.py b/repondeur/db_migrations/versions/08e2b571d519_add_conseils_team.py new file mode 100644 index 000000000..651a54624 --- /dev/null +++ b/repondeur/db_migrations/versions/08e2b571d519_add_conseils_team.py @@ -0,0 +1,27 @@ +"""Add conseils team + +Revision ID: 08e2b571d519 +Revises: fe0f5eb2cc56 +Create Date: 2020-03-13 11:51:03.175180 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "08e2b571d519" +down_revision = "fe0f5eb2cc56" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("conseils", sa.Column("team_pk", sa.Integer(), nullable=False)) + op.create_foreign_key( + op.f("conseils_team_pk_fkey"), "conseils", "teams", ["team_pk"], ["pk"] + ) + + +def downgrade(): + op.drop_constraint(op.f("conseils_team_pk_fkey"), "conseils", type_="foreignkey") + op.drop_column("conseils", "team_pk") diff --git a/repondeur/db_migrations/versions/243d9fa3359c_user_organisation.py b/repondeur/db_migrations/versions/243d9fa3359c_user_organisation.py new file mode 100644 index 000000000..288b011f8 --- /dev/null +++ b/repondeur/db_migrations/versions/243d9fa3359c_user_organisation.py @@ -0,0 +1,77 @@ +"""User organisation + +Revision ID: 243d9fa3359c +Revises: b09413ecb1a1 +Create Date: 2020-04-20 11:03:06.851174 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "243d9fa3359c" +down_revision = "b09413ecb1a1" +branch_labels = None +depends_on = None + + +def upgrade(): + op.drop_column("users_chambres", "organisation") + + organisation_table = op.create_table( + "organisations", + sa.Column("pk", sa.Integer(), nullable=False), + sa.Column("name", sa.Text(), nullable=False), + sa.PrimaryKeyConstraint("pk"), + sa.UniqueConstraint("name", name=op.f("organisations_name_key")), + ) + + # Fill up the table. + op.bulk_insert( + organisation_table, + [ + {"name": "Gouvernement"}, + {"name": "Employeurs territoriaux"}, + {"name": "CFE-CGC"}, + {"name": "CFTC"}, + {"name": "CGT"}, + {"name": "FA-FP"}, + {"name": "FSU"}, + {"name": "UNSA"}, + ], + ) + + # Set default organisation to `Gouvernement` for existing users memberships. + op.add_column( + "users_chambres", sa.Column("organisation_pk", sa.Integer(), nullable=True), + ) + op.execute( + """ + UPDATE users_chambres SET organisation_pk=( + SELECT pk FROM organisations WHERE name='Gouvernement' + ) + """ + ) + op.alter_column("users_chambres", "organisation_pk", nullable=False) + + op.create_foreign_key( + op.f("users_chambres_organisation_pk_fkey"), + "users_chambres", + "organisations", + ["organisation_pk"], + ["pk"], + ) + + +def downgrade(): + op.drop_constraint( + op.f("users_chambres_organisation_pk_fkey"), + "users_chambres", + type_="foreignkey", + ) + op.drop_column("users_chambres", "organisation_pk") + op.drop_table("organisations") + op.add_column( + "users_chambres", + sa.Column("organisation", sa.INTEGER(), autoincrement=False, nullable=True), + ) diff --git a/repondeur/db_migrations/versions/245e371cbf82_add_chambre_constraint.py b/repondeur/db_migrations/versions/245e371cbf82_add_chambre_constraint.py new file mode 100644 index 000000000..9afd4102d --- /dev/null +++ b/repondeur/db_migrations/versions/245e371cbf82_add_chambre_constraint.py @@ -0,0 +1,24 @@ +"""Add chambre constraint + +Revision ID: 245e371cbf82 +Revises: 95dfdcde1955 +Create Date: 2020-03-11 17:52:02.715512 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "245e371cbf82" +down_revision = "95dfdcde1955" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_check_constraint( + op.f("ck_conseils_chambre"), "conseils", "chambre NOT IN ('AN', 'SENAT')", + ) + + +def downgrade(): + op.drop_constraint(op.f("ck_conseils_chambre"), "conseils") diff --git a/repondeur/db_migrations/versions/3fe97532ba21_add_ccfp_chambre.py b/repondeur/db_migrations/versions/3fe97532ba21_add_ccfp_chambre.py new file mode 100644 index 000000000..248bcb261 --- /dev/null +++ b/repondeur/db_migrations/versions/3fe97532ba21_add_ccfp_chambre.py @@ -0,0 +1,27 @@ +"""Add CCFP chambre + +Revision ID: 3fe97532ba21 +Revises: f2882be74177 +Create Date: 2020-02-27 14:25:49.524626 + +""" +from alembic import op + +from zam_repondeur.models._helpers import alter_pg_enum + +# revision identifiers, used by Alembic. +revision = "3fe97532ba21" +down_revision = "f2882be74177" +branch_labels = None +depends_on = None + + +COLUMNS_TO_CONVERT = [("lectures", "chambre"), ("textes", "chambre")] + + +def upgrade(): + alter_pg_enum(op, "chambre", ["AN", "SENAT", "CCFP"], COLUMNS_TO_CONVERT) + + +def downgrade(): + alter_pg_enum(op, "chambre", ["AN", "SENAT"], COLUMNS_TO_CONVERT) diff --git a/repondeur/db_migrations/versions/95dfdcde1955_add_conseils.py b/repondeur/db_migrations/versions/95dfdcde1955_add_conseils.py new file mode 100644 index 000000000..11368f621 --- /dev/null +++ b/repondeur/db_migrations/versions/95dfdcde1955_add_conseils.py @@ -0,0 +1,43 @@ +"""Add conseils + +Revision ID: 95dfdcde1955 +Revises: 3fe97532ba21 +Create Date: 2020-03-11 15:56:00.842219 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import ENUM + +# revision identifiers, used by Alembic. +revision = "95dfdcde1955" +down_revision = "3fe97532ba21" +branch_labels = None +depends_on = None + + +formation_type = ENUM( + "ASSEMBLEE_PLENIERE", "FORMATION_SPECIALISEE", name="formation", create_type=False +) + + +def upgrade(): + formation_type.create(op.get_bind(), checkfirst=False) + op.create_table( + "conseils", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "chambre", + ENUM("AN", "SENAT", "CCFP", name="chambre", create_type=False), + nullable=False, + ), + sa.Column("date", sa.Date(), nullable=False), + sa.Column("formation", formation_type, nullable=False,), + sa.Column("urgence_declaree", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade(): + op.drop_table("conseils") + formation_type.drop(op.get_bind(), checkfirst=False) diff --git a/repondeur/db_migrations/versions/b09413ecb1a1_rename_conseils_to_seances.py b/repondeur/db_migrations/versions/b09413ecb1a1_rename_conseils_to_seances.py new file mode 100644 index 000000000..0d811d0f4 --- /dev/null +++ b/repondeur/db_migrations/versions/b09413ecb1a1_rename_conseils_to_seances.py @@ -0,0 +1,54 @@ +"""Rename conseils to seances + +Revision ID: b09413ecb1a1 +Revises: e39d4c9a9459 +Create Date: 2020-04-14 10:02:40.581873 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b09413ecb1a1" +down_revision = "e39d4c9a9459" +branch_labels = None +depends_on = None + + +def upgrade(): + op.drop_constraint( + "conseils_lectures_conseil_pk_fkey", "conseils_lectures", type_="foreignkey" + ) + op.rename_table(old_table_name="conseils", new_table_name="seances") + op.rename_table( + old_table_name="conseils_lectures", new_table_name="seances_lectures" + ) + op.alter_column("seances_lectures", "conseil_pk", new_column_name="seance_pk") + op.create_foreign_key( + op.f("seances_lectures_seance_pk_fkey"), + "seances_lectures", + "seances", + ["seance_pk"], + ["pk"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + + +def downgrade(): + op.drop_constraint( + "seances_lectures_seance_pk_fkey", "seances_lectures", type_="foreignkey" + ) + op.rename_table(old_table_name="seances", new_table_name="conseils") + op.rename_table( + old_table_name="seances_lectures", new_table_name="conseils_lectures" + ) + op.alter_column("conseils_lectures", "seance_pk", new_column_name="conseil_pk") + op.create_foreign_key( + op.f("conseils_lectures_conseil_pk_fkey"), + "conseils_lectures", + "conseils", + ["conseil_pk"], + ["pk"], + onupdate="CASCADE", + ondelete="CASCADE", + ) diff --git a/repondeur/db_migrations/versions/caaa6d0c7254_add_csfpe_chambre.py b/repondeur/db_migrations/versions/caaa6d0c7254_add_csfpe_chambre.py new file mode 100644 index 000000000..0e3fec30a --- /dev/null +++ b/repondeur/db_migrations/versions/caaa6d0c7254_add_csfpe_chambre.py @@ -0,0 +1,46 @@ +"""Add CSFPE chambre + +Revision ID: caaa6d0c7254 +Revises: 245e371cbf82 +Create Date: 2020-03-11 17:19:11.567332 + +""" +from contextlib import contextmanager + +from alembic import op + +from zam_repondeur.models._helpers import alter_pg_enum + +# revision identifiers, used by Alembic. +revision = "caaa6d0c7254" +down_revision = "245e371cbf82" +branch_labels = None +depends_on = None + + +COLUMNS_TO_CONVERT = [ + ("lectures", "chambre"), + ("textes", "chambre"), + ("conseils", "chambre"), +] + + +def upgrade(): + with release_check_constraint(): + alter_pg_enum( + op, "chambre", ["AN", "SENAT", "CCFP", "CSFPE"], COLUMNS_TO_CONVERT + ) + + +def downgrade(): + with release_check_constraint(): + alter_pg_enum(op, "chambre", ["AN", "SENAT", "CCFP"], COLUMNS_TO_CONVERT) + + +@contextmanager +def release_check_constraint(): + op.drop_constraint(op.f("ck_conseils_chambre"), "conseils") + yield + op.create_check_constraint( + op.f("ck_conseils_chambre"), "conseils", "chambre NOT IN ('AN', 'SENAT')", + ) diff --git a/repondeur/db_migrations/versions/e39d4c9a9459_rename_id_to_pk.py b/repondeur/db_migrations/versions/e39d4c9a9459_rename_id_to_pk.py new file mode 100644 index 000000000..9b1bdc4ae --- /dev/null +++ b/repondeur/db_migrations/versions/e39d4c9a9459_rename_id_to_pk.py @@ -0,0 +1,54 @@ +"""Rename id to pk + +Revision ID: e39d4c9a9459 +Revises: fb1d734cc205 +Create Date: 2020-04-14 08:44:22.021370 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "e39d4c9a9459" +down_revision = "fb1d734cc205" +branch_labels = None +depends_on = None + + +def upgrade(): + # Rename conseils.id + op.alter_column("conseils", "id", new_column_name="pk") + + # Rename conseils_lectures.conseil_id + op.drop_constraint( + "conseils_lectures_conseil_id_fkey", "conseils_lectures", type_="foreignkey" + ) + op.alter_column("conseils_lectures", "conseil_id", new_column_name="conseil_pk") + op.create_foreign_key( + op.f("conseils_lectures_conseil_pk_fkey"), + "conseils_lectures", + "conseils", + ["conseil_pk"], + ["pk"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + + +def downgrade(): + # Rename conseils_lectures.conseil_ipk + op.drop_constraint( + "conseils_lectures_conseil_pk_fkey", "conseils_lectures", type_="foreignkey" + ) + op.alter_column("conseils_lectures", "conseil_pk", new_column_name="conseil_id") + op.create_foreign_key( + op.f("conseils_lectures_conseil_id_fkey"), + "conseils_lectures", + "conseils", + ["conseil_id"], + ["pk"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + + # Rename conseils.pk + op.alter_column("conseils", "pk", new_column_name="id") diff --git a/repondeur/db_migrations/versions/f2882be74177_make_amendement_num_a_string.py b/repondeur/db_migrations/versions/f2882be74177_make_amendement_num_a_string.py new file mode 100644 index 000000000..386d79d21 --- /dev/null +++ b/repondeur/db_migrations/versions/f2882be74177_make_amendement_num_a_string.py @@ -0,0 +1,23 @@ +"""Make amendement num a string + +Revision ID: f2882be74177 +Revises: 9baaf0db7e30 +Create Date: 2020-02-26 17:02:48.798268 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f2882be74177" +down_revision = "9baaf0db7e30" +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column("amendements", "num", type_=sa.Text()) + + +def downgrade(): + op.alter_column("amendements", "num", type_=sa.Integer()) diff --git a/repondeur/db_migrations/versions/f4d9b3233d41_add_conseils_textes_order.py b/repondeur/db_migrations/versions/f4d9b3233d41_add_conseils_textes_order.py new file mode 100644 index 000000000..da5e68d0a --- /dev/null +++ b/repondeur/db_migrations/versions/f4d9b3233d41_add_conseils_textes_order.py @@ -0,0 +1,77 @@ +"""Add conseils textes order + +Revision ID: f4d9b3233d41 +Revises: 08e2b571d519 +Create Date: 2020-03-23 14:59:26.902024 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f4d9b3233d41" +down_revision = "08e2b571d519" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "conseils_lectures", sa.Column("position", sa.Integer(), nullable=True) + ) + op.create_primary_key( + "pk_conseils_lecture", "conseils_lectures", ["conseil_id", "lecture_pk"] + ) + op.drop_constraint( + "conseils_lectures_conseil_id_fkey", "conseils_lectures", type_="foreignkey" + ) + op.drop_constraint( + "conseils_lectures_lecture_pk_fkey", "conseils_lectures", type_="foreignkey" + ) + op.create_foreign_key( + op.f("conseils_lectures_lecture_pk_fkey"), + "conseils_lectures", + "lectures", + ["lecture_pk"], + ["pk"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_foreign_key( + op.f("conseils_lectures_conseil_id_fkey"), + "conseils_lectures", + "conseils", + ["conseil_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + + +def downgrade(): + op.drop_constraint( + op.f("conseils_lectures_conseil_id_fkey"), + "conseils_lectures", + type_="foreignkey", + ) + op.drop_constraint( + op.f("conseils_lectures_lecture_pk_fkey"), + "conseils_lectures", + type_="foreignkey", + ) + op.create_foreign_key( + "conseils_lectures_lecture_pk_fkey", + "conseils_lectures", + "lectures", + ["lecture_pk"], + ["pk"], + ) + op.create_foreign_key( + "conseils_lectures_conseil_id_fkey", + "conseils_lectures", + "conseils", + ["conseil_id"], + ["id"], + ) + op.drop_constraint("pk_conseils_lecture", "conseils_lectures") + op.drop_column("conseils_lectures", "position") diff --git a/repondeur/db_migrations/versions/fb1d734cc205_user_membership.py b/repondeur/db_migrations/versions/fb1d734cc205_user_membership.py new file mode 100644 index 000000000..2c6560092 --- /dev/null +++ b/repondeur/db_migrations/versions/fb1d734cc205_user_membership.py @@ -0,0 +1,40 @@ +"""User membership + +Revision ID: fb1d734cc205 +Revises: f4d9b3233d41 +Create Date: 2020-04-07 11:30:57.329120 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "fb1d734cc205" +down_revision = "f4d9b3233d41" +branch_labels = None +depends_on = None + + +def upgrade(): + chambre_type = postgresql.ENUM( + "AN", "SENAT", "CCFP", "CSFPE", name="chambre", create_type=False + ) + op.create_table( + "users_chambres", + sa.Column("user_pk", sa.Integer(), nullable=False), + sa.Column("chambre", chambre_type, nullable=False,), + sa.Column("organisation", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["user_pk"], + ["users.pk"], + name=op.f("users_chambres_user_pk_fkey"), + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("user_pk", "chambre"), + ) + + +def downgrade(): + op.drop_table("users_chambres") diff --git a/repondeur/db_migrations/versions/fe0f5eb2cc56_add_conseils_lectures_relationship.py b/repondeur/db_migrations/versions/fe0f5eb2cc56_add_conseils_lectures_relationship.py new file mode 100644 index 000000000..4be16c14d --- /dev/null +++ b/repondeur/db_migrations/versions/fe0f5eb2cc56_add_conseils_lectures_relationship.py @@ -0,0 +1,37 @@ +"""Add conseils lectures relationship + +Revision ID: fe0f5eb2cc56 +Revises: caaa6d0c7254 +Create Date: 2020-03-13 11:27:03.486594 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "fe0f5eb2cc56" +down_revision = "caaa6d0c7254" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "conseils_lectures", + sa.Column("conseil_id", sa.Integer(), nullable=True), + sa.Column("lecture_pk", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["conseil_id"], + ["conseils.id"], + name=op.f("conseils_lectures_conseil_id_fkey"), + ), + sa.ForeignKeyConstraint( + ["lecture_pk"], + ["lectures.pk"], + name=op.f("conseils_lectures_lecture_pk_fkey"), + ), + ) + + +def downgrade(): + op.drop_table("conseils_lectures") diff --git a/repondeur/development-docker.ini b/repondeur/development-docker.ini index 6aa268e2c..3b8c2f5aa 100644 --- a/repondeur/development-docker.ini +++ b/repondeur/development-docker.ini @@ -1,5 +1,5 @@ [app:main] -use = egg:zam_repondeur +use = egg:zam_repondeur#visam pyramid.includes = pyramid_mailer.debug @@ -83,6 +83,7 @@ Amendement = zam_repondeur.models.Amendement Article = zam_repondeur.models.Article Batch = zam_repondeur.models.Batch Chambre = zam_repondeur.models.Chambre +Conseil = zam_repondeur.visam.models.Conseil DBSession = zam_repondeur.models.DBSession Dossier = zam_repondeur.models.Dossier Lecture = zam_repondeur.models.Lecture @@ -91,6 +92,7 @@ Team = zam_repondeur.models.Team Texte = zam_repondeur.models.Texte User = zam_repondeur.models.User repository = zam_repondeur.services.data.repository +transaction = transaction ### # logging configuration diff --git a/repondeur/development.ini b/repondeur/development.ini index ead57a25e..c4fca058b 100644 --- a/repondeur/development.ini +++ b/repondeur/development.ini @@ -1,5 +1,5 @@ [app:main] -use = egg:zam_repondeur +use = egg:zam_repondeur#visam pyramid.includes = pyramid_mailer.debug @@ -84,6 +84,7 @@ Amendement = zam_repondeur.models.Amendement Article = zam_repondeur.models.Article Batch = zam_repondeur.models.Batch Chambre = zam_repondeur.models.Chambre +Conseil = zam_repondeur.visam.models.Conseil DBSession = zam_repondeur.models.DBSession Dossier = zam_repondeur.models.Dossier Lecture = zam_repondeur.models.Lecture @@ -92,6 +93,7 @@ Team = zam_repondeur.models.Team Texte = zam_repondeur.models.Texte User = zam_repondeur.models.User repository = zam_repondeur.services.data.repository +transaction = transaction ### # logging configuration diff --git a/repondeur/production.ini.template b/repondeur/production.ini.template index 5df9acbd4..264d601d7 100644 --- a/repondeur/production.ini.template +++ b/repondeur/production.ini.template @@ -19,7 +19,7 @@ root = %(here)s [app:repondeur] -use = egg:zam_repondeur +use = egg:zam_repondeur#visam pyramid.includes = pyramid_mailer @@ -98,6 +98,7 @@ Amendement = zam_repondeur.models.Amendement Article = zam_repondeur.models.Article Batch = zam_repondeur.models.Batch Chambre = zam_repondeur.models.Chambre +Conseil = zam_repondeur.visam.models.Conseil DBSession = zam_repondeur.models.DBSession Dossier = zam_repondeur.models.Dossier Lecture = zam_repondeur.models.Lecture @@ -106,6 +107,7 @@ Team = zam_repondeur.models.Team Texte = zam_repondeur.models.Texte User = zam_repondeur.models.User repository = zam_repondeur.services.data.repository +transaction = transaction ### diff --git a/repondeur/setup.py b/repondeur/setup.py index 7f04b6c69..04fa0c982 100644 --- a/repondeur/setup.py +++ b/repondeur/setup.py @@ -44,7 +44,10 @@ url="https://github.com/betagouv/zam", install_requires=requires, entry_points={ - "paste.app_factory": ["main = zam_repondeur:make_app"], + "paste.app_factory": [ + "main = zam_repondeur:make_app", + "visam = zam_repondeur.visam.app:make_app", + ], "console_scripts": [ "zam_worker = zam_repondeur.scripts.worker:main", "zam_fetch_amendements = zam_repondeur.scripts.fetch_amendements:main", @@ -56,6 +59,7 @@ "zam_queue = zam_repondeur.scripts.queue:main", "zam_update_dossiers = zam_repondeur.scripts.update_dossiers:main", "zam_fake = zam_repondeur.scripts.fake:main", + "visam_colonnes = zam_repondeur.scripts.enfoncer_colonnes:main", ], }, ) diff --git a/repondeur/stubs/pyramid/request.pyi b/repondeur/stubs/pyramid/request.pyi index f3418def9..1d3724e2a 100644 --- a/repondeur/stubs/pyramid/request.pyi +++ b/repondeur/stubs/pyramid/request.pyi @@ -20,6 +20,7 @@ class Request: session: Any GET: Any POST: Any + json_body: dict params: Any unauthenticated_userid: Optional[str] team: Team diff --git a/repondeur/tests/conftest.py b/repondeur/tests/conftest.py index 33a1227bb..51ff74ca5 100644 --- a/repondeur/tests/conftest.py +++ b/repondeur/tests/conftest.py @@ -1,11 +1,9 @@ import os -from contextlib import contextmanager from pathlib import Path import pytest import responses import transaction -from pyramid.threadlocal import get_current_registry from pyramid_mailer import get_mailer from fixtures.dossiers import * # noqa: F401,F403 @@ -18,50 +16,16 @@ from fixtures.scraping import * # noqa: F401,F403 from fixtures.shared_tables import * # noqa: F401,F403 from fixtures.users import * # noqa: F401,F403 -from testapp import TestApp as BaseTestApp +from testapp import TestApp HERE = Path(__file__) -class TestApp(BaseTestApp): - def get(self, *args, **kwargs): - with self.auto_login(kwargs): - return super().get(*args, **kwargs) - - def post(self, *args, **kwargs): - with self.auto_login(kwargs): - return super().post(*args, **kwargs) - - def post_json(self, *args, **kwargs): - with self.auto_login(kwargs): - return super().post_json(*args, **kwargs) - - @contextmanager - def auto_login(self, kwargs): - from zam_repondeur.models import User - - user = kwargs.pop("user", None) - if user is not None: - assert isinstance(user, User) - self.user_login(email=user.email, headers=kwargs.get("headers")) - - yield - - def user_login(self, email, headers=None): - from zam_repondeur.auth import generate_auth_token - from zam_repondeur.services.users import repository - - token = generate_auth_token() - repository.set_auth_token(email, token) - resp = self.get("/authentification", params={"token": token}, headers=headers) - assert resp.status_code == 302 - - @pytest.fixture(scope="session") def settings(tmp_path_factory): return { "pyramid.debug_authorization": True, - "pyramid.includes": "pyramid_mailer.testing", + "pyramid.includes": ["pyramid_mailer.testing"], "sqlalchemy.url": os.environ.get( "ZAM_TEST_DB_URL", "postgresql://zam@localhost/zam-test" ), @@ -230,9 +194,10 @@ def app( @pytest.fixture -def mailer(): - registry = get_current_registry() - yield get_mailer(registry) +def mailer(app): + mailer = get_mailer(app.app.registry) + mailer.outbox = [] # clean the list of sent messages first + return mailer def pytest_runtest_call(item): @@ -241,16 +206,9 @@ def pytest_runtest_call(item): See: https://docs.pytest.org/en/latest/reference.html#hook-reference """ - clear_email_outbox() clear_rate_limiting_counters() -def clear_email_outbox(): - registry = get_current_registry() - mailer = get_mailer(registry) - mailer.outbox = [] - - def clear_rate_limiting_counters(): """ This prevents throttling caused by the many logins from automatic tests diff --git a/repondeur/tests/fetch/test_amendements.py b/repondeur/tests/fetch/test_amendements.py new file mode 100644 index 000000000..7119205b7 --- /dev/null +++ b/repondeur/tests/fetch/test_amendements.py @@ -0,0 +1,11 @@ +import pytest + +from zam_repondeur.models.chambre import Chambre + + +class TestRemoteSource: + @pytest.mark.parametrize("chambre", [Chambre.CCFP, Chambre.CSFPE]) + def test_conseil_de_la_fonction_publique(self, chambre): + from zam_repondeur.services.fetch.amendements import RemoteSource + + assert RemoteSource.get_remote_source_for_chambre(chambre, settings={}) is None diff --git a/repondeur/tests/fetch/test_division.py b/repondeur/tests/fetch/test_division.py index 7aa11bb99..999929b5d 100644 --- a/repondeur/tests/fetch/test_division.py +++ b/repondeur/tests/fetch/test_division.py @@ -285,7 +285,6 @@ def test_parse_subdiv_art_add_av_texte_title(texte_plfss2018_an_premiere_lecture subdiv = parse_subdiv( "art. add. avant PROJET DE LOI de financement de la sécurité sociale pour 2018", - texte=texte_plfss2018_an_premiere_lecture, ) assert subdiv == SubDiv("titre", "", "", "avant") @@ -293,7 +292,5 @@ def test_parse_subdiv_art_add_av_texte_title(texte_plfss2018_an_premiere_lecture def test_parse_subdiv_error(texte_plfss2018_an_premiere_lecture): from zam_repondeur.services.fetch.division import parse_subdiv - subdiv = parse_subdiv( - "this is unparsable garbage", texte=texte_plfss2018_an_premiere_lecture - ) + subdiv = parse_subdiv("this is unparsable garbage") assert subdiv == SubDiv("erreur", "", "", "") diff --git a/repondeur/tests/fetch/test_fetch_amendements.py b/repondeur/tests/fetch/test_fetch_amendements.py index 83d19affb..982fe4280 100644 --- a/repondeur/tests/fetch/test_fetch_amendements.py +++ b/repondeur/tests/fetch/test_fetch_amendements.py @@ -154,12 +154,12 @@ def test_fetch_amendements_senat( result = source_senat.fetch(lecture_senat) - assert result.fetched == {6666, 7777, 9999} - assert result.created == {7777} + assert result.fetched == {"6666", "7777", "9999"} + assert result.created == {"7777"} assert result.errored == set() # Check that the response was preserved on the updated amendement - amendement = DBSession.query(Amendement).filter(Amendement.num == 9999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "9999").one() assert amendement.user_content.avis == "Favorable" assert amendement.user_content.objet == "Objet" assert amendement.user_content.reponse == "Réponse" @@ -168,7 +168,7 @@ def test_fetch_amendements_senat( assert amendement.position == 3 # Check that the position is set for the new amendement - amendement = DBSession.query(Amendement).filter(Amendement.num == 7777).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "7777").one() assert amendement.position == 2 @@ -176,12 +176,12 @@ def test_fetch_amendements_an(app, source_an, lecture_an, article1_an): from zam_repondeur.models import Amendement, DBSession from zam_repondeur.services.fetch.an.amendements import ANDerouleurData - Amendement.create(lecture=lecture_an, article=article1_an, num=6, position=1) + Amendement.create(lecture=lecture_an, article=article1_an, num="6", position=1) amendement_9 = Amendement.create( lecture=lecture_an, article=article1_an, - num=9, + num="9", position=2, avis="Favorable", objet="Objet", @@ -267,11 +267,11 @@ def dynamic_return_value(urls, force_list=None): result = source_an.fetch(lecture_an) - assert result.fetched == {6, 7, 9} - assert result.created == {7} + assert result.fetched == {"6", "7", "9"} + assert result.created == {"7"} assert result.errored == set() - amendement_9 = DBSession.query(Amendement).filter(Amendement.num == 9).one() + amendement_9 = DBSession.query(Amendement).filter(Amendement.num == "9").one() # Check that the response was preserved on the updated amendement assert amendement_9.user_content.avis == "Favorable" assert amendement_9.user_content.objet == "Objet" @@ -286,7 +286,7 @@ def dynamic_return_value(urls, force_list=None): assert amendement_9.position == 3 # Check that the position was set for the new amendement - amendement_7 = DBSession.query(Amendement).filter(Amendement.num == 7).one() + amendement_7 = DBSession.query(Amendement).filter(Amendement.num == "7").one() assert amendement_7.position == 2 @@ -294,12 +294,12 @@ def test_fetch_amendements_an_with_mission(app, source_an, lecture_an, article1_ from zam_repondeur.models import Amendement, DBSession from zam_repondeur.services.fetch.an.amendements import ANDerouleurData - Amendement.create(lecture=lecture_an, article=article1_an, num=6, position=1) + Amendement.create(lecture=lecture_an, article=article1_an, num="6", position=1) amendement_9 = Amendement.create( lecture=lecture_an, article=article1_an, - num=9, + num="9", position=2, avis="Favorable", objet="Objet", @@ -386,11 +386,11 @@ def dynamic_return_value(urls, force_list=None): result = source_an.fetch(lecture_an) - assert result.fetched == {6, 7, 9} - assert result.created == {7} + assert result.fetched == {"6", "7", "9"} + assert result.created == {"7"} assert result.errored == set() - amendement_9 = DBSession.query(Amendement).filter(Amendement.num == 9).one() + amendement_9 = DBSession.query(Amendement).filter(Amendement.num == "9").one() # Check that the mission is created assert amendement_9.mission_titre == "Mission « Outre-mer »" assert amendement_9.mission_titre_court == "Outre-mer" @@ -403,14 +403,14 @@ def test_fetch_amendements_an_without_auteur_key( from zam_repondeur.services.fetch.an.amendements import ANDerouleurData amendement_6 = Amendement.create( - lecture=lecture_an, article=article1_an, num=6, position=1 + lecture=lecture_an, article=article1_an, num="6", position=1 ) DBSession.add(amendement_6) amendement_9 = Amendement.create( lecture=lecture_an, article=article1_an, - num=9, + num="9", position=2, avis="Favorable", objet="Objet", @@ -490,18 +490,18 @@ def dynamic_return_value(urls, force_list=None): result = source_an.fetch(lecture_an) - assert result.fetched == {6, 7, 9} - assert result.created == {7} + assert result.fetched == {"6", "7", "9"} + assert result.created == {"7"} assert result.errored == set() - for num in [6, 7, 9]: + for num in ["6", "7", "9"]: assert any( record.levelname == "WARNING" and record.message.startswith(f"Unknown auteur for amendement {num}") for record in caplog.records ) - amendement_9 = DBSession.query(Amendement).filter(Amendement.num == 9).one() + amendement_9 = DBSession.query(Amendement).filter(Amendement.num == "9").one() # Check that the missing auteur key leads to an explicit string assert amendement_9.matricule == "" assert amendement_9.groupe == "Non trouvé" @@ -515,14 +515,14 @@ def test_fetch_amendements_an_without_group_tribun_id( from zam_repondeur.services.fetch.an.amendements import ANDerouleurData amendement_6 = Amendement.create( - lecture=lecture_an, article=article1_an, num=6, position=1 + lecture=lecture_an, article=article1_an, num="6", position=1 ) DBSession.add(amendement_6) amendement_9 = Amendement.create( lecture=lecture_an, article=article1_an, - num=9, + num="9", position=2, avis="Favorable", objet="Objet", @@ -610,11 +610,11 @@ def dynamic_return_value(urls, force_list=None): result = source_an.fetch(lecture_an) - assert result.fetched == {6, 7, 9} - assert result.created == {7} + assert result.fetched == {"6", "7", "9"} + assert result.created == {"7"} assert result.errored == set() - for num in [6, 7, 9]: + for num in ["6", "7", "9"]: assert any( record.levelname == "WARNING" and record.message.startswith( @@ -623,7 +623,7 @@ def dynamic_return_value(urls, force_list=None): for record in caplog.records ) - amendement_9 = DBSession.query(Amendement).filter(Amendement.num == 9).one() + amendement_9 = DBSession.query(Amendement).filter(Amendement.num == "9").one() # Check that the empty group key leads to an explicit string assert amendement_9.matricule == "642788" assert amendement_9.groupe == "Non précisé" @@ -637,14 +637,14 @@ def test_fetch_amendements_an_with_unknown_group_tribun_id( from zam_repondeur.services.fetch.an.amendements import ANDerouleurData amendement_6 = Amendement.create( - lecture=lecture_an, article=article1_an, num=6, position=1 + lecture=lecture_an, article=article1_an, num="6", position=1 ) DBSession.add(amendement_6) amendement_9 = Amendement.create( lecture=lecture_an, article=article1_an, - num=9, + num="9", position=2, avis="Favorable", objet="Objet", @@ -732,11 +732,11 @@ def dynamic_return_value(urls, force_list=None): result = source_an.fetch(lecture_an) - assert result.fetched == {6, 7, 9} - assert result.created == {7} + assert result.fetched == {"6", "7", "9"} + assert result.created == {"7"} assert result.errored == set() - for num in [6, 7, 9]: + for num in ["6", "7", "9"]: assert any( record.levelname == "WARNING" and record.message.startswith( @@ -745,7 +745,7 @@ def dynamic_return_value(urls, force_list=None): for record in caplog.records ) - amendement_9 = DBSession.query(Amendement).filter(Amendement.num == 9).one() + amendement_9 = DBSession.query(Amendement).filter(Amendement.num == "9").one() # Check that the wrong group key leads to an explicit string assert amendement_9.matricule == "642788" assert amendement_9.groupe == "Non trouvé" @@ -801,7 +801,7 @@ def test_fetch_amendements_with_errored( assert result.fetched == set() assert result.created == set() - assert result.errored == {6, 7, 9} + assert result.errored == {"6", "7", "9"} assert DBSession.query(Amendement).count() == len(amendements_an) == 2 @@ -879,7 +879,7 @@ def test_fetch_amendements_with_connection_errors( assert result.fetched == set() assert result.created == set() - assert result.errored == {6, 7, 9} + assert result.errored == {"6", "7", "9"} assert DBSession.query(Amendement).count() == len(amendements_an) == 2 @@ -889,7 +889,7 @@ def test_fetch_update_amendements_an_with_batch_preserve_batch( from zam_repondeur.models import Amendement, DBSession from zam_repondeur.services.fetch.an.amendements import ANDerouleurData - assert amendements_an_batch[0].location.batch.nums == [666, 999] + assert amendements_an_batch[0].location.batch.nums == ["666", "999"] with transaction.manager, patch( "zam_repondeur.services.fetch.an.amendements.fetch_discussion_list" @@ -965,12 +965,12 @@ def dynamic_return_value(urls, force_list=None): result = source_an.fetch(lecture_an) - assert result.fetched == {666, 999} + assert result.fetched == {"666", "999"} assert result.created == set() assert result.errored == set() - amendement_666 = DBSession.query(Amendement).filter(Amendement.num == 666).one() - assert amendement_666.location.batch.nums == [666, 999] + amendement_666 = DBSession.query(Amendement).filter(Amendement.num == "666").one() + assert amendement_666.location.batch.nums == ["666", "999"] def test_fetch_update_amendements_an_with_batch_and_changing_article( @@ -980,7 +980,7 @@ def test_fetch_update_amendements_an_with_batch_and_changing_article( from zam_repondeur.models.events.amendement import BatchUnset from zam_repondeur.services.fetch.an.amendements import ANDerouleurData - assert amendements_an_batch[0].location.batch.nums == [666, 999] + assert amendements_an_batch[0].location.batch.nums == ["666", "999"] with transaction.manager, patch( "zam_repondeur.services.fetch.an.amendements.fetch_discussion_list" @@ -1056,11 +1056,11 @@ def dynamic_return_value(urls, force_list=None): result = source_an.fetch(lecture_an) - assert result.fetched == {666, 999} + assert result.fetched == {"666", "999"} assert result.created == set() assert result.errored == set() - for num in [666, 999]: + for num in ["666", "999"]: amendement = DBSession.query(Amendement).filter(Amendement.num == num).one() assert amendement.location.batch is None diff --git a/repondeur/tests/fetch/test_fetch_an.py b/repondeur/tests/fetch/test_fetch_an.py index 4de2593bc..766b9e9b9 100644 --- a/repondeur/tests/fetch/test_fetch_an.py +++ b/repondeur/tests/fetch/test_fetch_an.py @@ -62,7 +62,7 @@ def test_simple_amendements(self, lecture_an, app, source): assert len(amendements) == 5 - assert amendements[0].num == 177 + assert amendements[0].num == "177" assert amendements[0].position == 1 assert ( amendements[0].tri_amendement @@ -71,7 +71,7 @@ def test_simple_amendements(self, lecture_an, app, source): assert amendements[0].id_discussion_commune is None assert amendements[0].id_identique == 20386 - assert amendements[1].num == 270 + assert amendements[1].num == "270" assert amendements[1].position == 2 assert ( amendements[1].tri_amendement @@ -80,7 +80,7 @@ def test_simple_amendements(self, lecture_an, app, source): assert amendements[1].id_discussion_commune is None assert amendements[1].id_identique == 20386 - assert amendements[2].num == 723 + assert amendements[2].num == "723" assert amendements[2].position == 3 assert ( amendements[2].tri_amendement @@ -89,7 +89,7 @@ def test_simple_amendements(self, lecture_an, app, source): assert amendements[2].id_discussion_commune is None assert amendements[2].id_identique is None - assert amendements[3].num == 135 + assert amendements[3].num == "135" assert amendements[3].position == 4 assert ( amendements[3].tri_amendement @@ -98,7 +98,7 @@ def test_simple_amendements(self, lecture_an, app, source): assert amendements[3].id_discussion_commune is None assert amendements[3].id_identique is None - assert amendements[4].num == 192 + assert amendements[4].num == "192" assert amendements[4].position == 5 assert ( amendements[4].tri_amendement @@ -107,7 +107,7 @@ def test_simple_amendements(self, lecture_an, app, source): assert amendements[4].id_discussion_commune is None assert amendements[4].id_identique == 20439 - assert result.created == {177, 270, 723, 135, 192} + assert result.created == {"177", "270", "723", "135", "192"} assert result.errored == set() @responses.activate @@ -144,7 +144,7 @@ def test_simple_amendements_progress_status( assert lecture_an.get_fetch_progress() == {} assert len(result.fetched) == 5 - assert result.created == {177, 270, 723, 135, 192} + assert result.created == {"177", "270", "723", "135", "192"} assert result.errored == set() @responses.activate @@ -193,7 +193,7 @@ def test_fetch_amendements_not_in_discussion_list(self, lecture_an, app, source) assert len(amendements) == 2 - assert amendements[0].num == 177 + assert amendements[0].num == "177" assert amendements[0].position == 1 assert ( amendements[0].tri_amendement @@ -202,7 +202,7 @@ def test_fetch_amendements_not_in_discussion_list(self, lecture_an, app, source) assert amendements[0].id_discussion_commune is None assert amendements[0].id_identique == 20386 - assert amendements[1].num == 192 + assert amendements[1].num == "192" assert amendements[1].position is None assert ( amendements[1].tri_amendement @@ -211,7 +211,7 @@ def test_fetch_amendements_not_in_discussion_list(self, lecture_an, app, source) assert amendements[1].id_discussion_commune is None assert amendements[1].id_identique is None - assert result.created == {177, 192} + assert result.created == {"177", "192"} assert result.errored == set() @responses.activate @@ -234,21 +234,21 @@ def test_commission(self, lecture_an, app, source): assert len(amendements) == 2 - assert amendements[0].num == 2 + assert amendements[0].num == "2" assert amendements[0].position is None assert ( amendements[0].tri_amendement == "ilaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaf" ) - assert amendements[1].num == 1 + assert amendements[1].num == "1" assert amendements[1].position == 1 assert ( amendements[1].tri_amendement == "elaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" ) - assert result.created == {1, 2} + assert result.created == {"1", "2"} assert result.errored == set() @responses.activate @@ -284,7 +284,7 @@ def test_sous_amendements( assert len(amendements) == 3 - assert amendements[0].num == 1 + assert amendements[0].num == "1" assert amendements[0].position == 1 assert ( amendements[0].tri_amendement @@ -293,7 +293,7 @@ def test_sous_amendements( assert amendements[0].id_discussion_commune == 3448 assert amendements[0].id_identique == 8496 - assert amendements[1].num == 2 + assert amendements[1].num == "2" assert amendements[1].position == 2 assert ( amendements[1].tri_amendement @@ -302,7 +302,7 @@ def test_sous_amendements( assert amendements[1].id_discussion_commune is None assert amendements[1].id_identique is None - assert amendements[2].num == 3 + assert amendements[2].num == "3" assert amendements[2].position == 3 assert ( amendements[2].tri_amendement @@ -315,7 +315,7 @@ def test_sous_amendements( assert amendement.parent is amendements[0] assert amendement.parent_pk == amendements[0].pk - assert result.created == {1, 2, 3} + assert result.created == {"1", "2", "3"} assert result.errored == set() @responses.activate @@ -340,14 +340,14 @@ def test_with_404(self, lecture_an, app, source): amendements = sorted(Amendement.get(lecture_an, num) for num in result.fetched) assert len(amendements) == 4 - assert amendements[0].num == 177 - assert amendements[1].num == 723 - assert amendements[2].num == 135 - assert amendements[3].num == 192 + assert amendements[0].num == "177" + assert amendements[1].num == "723" + assert amendements[2].num == "135" + assert amendements[3].num == "192" assert [amdt.position for amdt in amendements] == [1, 3, 4, 5] - assert result.created == {177, 723, 135, 192} - assert result.errored == {270} + assert result.created == {"177", "723", "135", "192"} + assert result.errored == {"270"} class TestGetOrganeAbrev: @@ -478,7 +478,7 @@ def test_simple_amendement(self, lecture_an, app, source): assert created assert amendement.lecture == lecture_an - assert amendement.num == 177 + assert amendement.num == "177" assert amendement.rectif == 0 assert amendement.auteur == "Door Jean-Pierre" assert amendement.matricule == "267289" diff --git a/repondeur/tests/fetch/test_fetch_articles.py b/repondeur/tests/fetch/test_fetch_articles.py index 698cfbd8d..599ab6406 100644 --- a/repondeur/tests/fetch/test_fetch_articles.py +++ b/repondeur/tests/fetch/test_fetch_articles.py @@ -153,7 +153,7 @@ def test_existing_articles_are_updated(self, app, lecture_an, amendements_an): DBSession.add(lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.article.user_content.title == "" assert amendement.article.content == {} @@ -162,7 +162,7 @@ def test_existing_articles_are_updated(self, app, lecture_an, amendements_an): assert changed # We can get the article contents from an amendement - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert ( amendement.article.user_content.title == "Dispositions relatives l'exercice 2016" @@ -233,7 +233,7 @@ def test_custom_article_titles_are_preserved(self, app, lecture_an, amendements_ DBSession.add(lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.article.user_content.title == "" assert amendement.article.content == {} @@ -245,7 +245,7 @@ def test_custom_article_titles_are_preserved(self, app, lecture_an, amendements_ assert changed # We can get the article contents from an amendement - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.article.user_content.title == "My custom title" assert amendement.article.content["001"].startswith( "Au titre de l'exercice 2016" @@ -323,7 +323,7 @@ def test_fallback_to_alternative_url_pattern( assert changed - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.article.content["001"].startswith( "Le code des relations entre" ) @@ -353,7 +353,7 @@ def test_not_found(self, app, lecture_an, amendements_an): assert not changed - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.article.content == {} @@ -383,7 +383,9 @@ def test_get_articles_senat( assert changed - amendement = DBSession.query(Amendement).filter(Amendement.num == 6666).first() + amendement = ( + DBSession.query(Amendement).filter(Amendement.num == "6666").first() + ) assert amendement.article.content["001"].startswith( "Au titre de l'exercice 2016" ) @@ -441,7 +443,9 @@ def test_get_articles_senat_with_mult( assert changed - amendement = DBSession.query(Amendement).filter(Amendement.num == 6666).first() + amendement = ( + DBSession.query(Amendement).filter(Amendement.num == "6666").first() + ) assert amendement.article.content["001"].startswith("Ne donnent pas lieu à") @responses.activate @@ -465,7 +469,9 @@ def test_get_articles_senat_with_dots( assert changed - amendement = DBSession.query(Amendement).filter(Amendement.num == 6666).first() + amendement = ( + DBSession.query(Amendement).filter(Amendement.num == "6666").first() + ) assert amendement.article.content["001"].startswith("La stratégie nationale") @responses.activate @@ -489,7 +495,9 @@ def test_get_articles_missing_data( assert not changed - amendement = DBSession.query(Amendement).filter(Amendement.num == 6666).first() + amendement = ( + DBSession.query(Amendement).filter(Amendement.num == "6666").first() + ) assert amendement.article.content == {} # Events should NOT be created @@ -516,7 +524,9 @@ def test_get_articles_tlfp_parse_error( assert not changed - amendement = DBSession.query(Amendement).filter(Amendement.num == 6666).first() + amendement = ( + DBSession.query(Amendement).filter(Amendement.num == "6666").first() + ) assert amendement.article.content == {} # Events should NOT be created diff --git a/repondeur/tests/fetch/test_fetch_senat.py b/repondeur/tests/fetch/test_fetch_senat.py index 9bb647b95..ada87ae4f 100644 --- a/repondeur/tests/fetch/test_fetch_senat.py +++ b/repondeur/tests/fetch/test_fetch_senat.py @@ -176,9 +176,9 @@ def test_aspire_senat(app, lecture_senat, settings): assert len(result.fetched) == 595 # Check details of #1 - assert 1 in result.fetched - amendement = Amendement.get(lecture_senat, 1) - assert amendement.num == 1 + assert "1" in result.fetched + amendement = Amendement.get(lecture_senat, "1") + assert amendement.num == "1" assert amendement.rectif == 1 assert amendement.article.num == "7" assert amendement.article.pos == "après" @@ -211,10 +211,10 @@ def test_aspire_senat(app, lecture_senat, settings): assert events[2].render_summary() == "L’amendement a été rectifié." # Check that #596 has a parent - assert 596 in result.fetched - sous_amendement = Amendement.get(lecture_senat, 596) + assert "596" in result.fetched + sous_amendement = Amendement.get(lecture_senat, "596") assert sous_amendement.parent is not None - assert sous_amendement.parent.num == 229 + assert sous_amendement.parent.num == "229" assert sous_amendement.parent.rectif == 1 @@ -268,13 +268,13 @@ def test_aspire_senat_again_with_irrecevable(app, lecture_senat, settings): source = Senat(settings=settings) result = source.fetch(lecture_senat) - assert 1 in result.fetched - amendement = Amendement.get(lecture_senat, 1) + assert "1" in result.fetched + amendement = Amendement.get(lecture_senat, "1") assert len(amendement.events) == 3 result = source.fetch(lecture_senat) - assert 1 in result.fetched - amendement = Amendement.get(lecture_senat, 1) + assert "1" in result.fetched + amendement = Amendement.get(lecture_senat, "1") assert len(amendement.events) == 4 assert isinstance(amendement.events[3], AmendementIrrecevable) @@ -344,8 +344,8 @@ def test_aspire_senat_again_with_irrecevable_transfers_to_index( # Let's fetch a new amendement result = source.fetch(lecture_senat) - assert 1 in result.fetched - amendement = Amendement.get(lecture_senat, 1) + assert "1" in result.fetched + amendement = Amendement.get(lecture_senat, "1") assert len(amendement.events) == 3 # Put it on a user table @@ -356,8 +356,8 @@ def test_aspire_senat_again_with_irrecevable_transfers_to_index( # Now fetch the same amendement again (now irrecevable) result = source.fetch(lecture_senat) - assert 1 in result.fetched - amendement = Amendement.get(lecture_senat, 1) + assert "1" in result.fetched + amendement = Amendement.get(lecture_senat, "1") assert len(amendement.events) == 5 # two more # An irrecevable event has been created @@ -422,7 +422,7 @@ def test_aspire_senat_plf2019_1re_partie(app, lecture_plf_1re_partie, settings): assert len(result.fetched) == 1005 # Missions are not set on first part - amendement = Amendement.get(lecture_plf_1re_partie, 1) + amendement = Amendement.get(lecture_plf_1re_partie, "1") assert amendement.mission_titre is None diff --git a/repondeur/tests/fetch/test_parse.py b/repondeur/tests/fetch/test_parse.py index baf2dc5bb..3ff199d36 100644 --- a/repondeur/tests/fetch/test_parse.py +++ b/repondeur/tests/fetch/test_parse.py @@ -29,7 +29,7 @@ def test_parse_from_csv(lecture_senat, settings): amendement, created = source.parse_from_csv(amend, lecture_senat) assert created - assert amendement.num == 1 + assert amendement.num == "1" assert amendement.rectif == 1 assert amendement.num_disp == "1 rect." assert amendement.date_depot == date(2017, 11, 13) @@ -72,7 +72,7 @@ def test_parse_from_csv_unparsable_article(lecture_senat, settings): amendement, created = source.parse_from_csv(amend, lecture_senat) assert created - assert amendement.num == 1 + assert amendement.num == "1" assert amendement.rectif == 1 assert amendement.num_disp == "1 rect." diff --git a/repondeur/tests/fetch/test_update_an.py b/repondeur/tests/fetch/test_update_an.py index 8963d9877..2318de3bb 100644 --- a/repondeur/tests/fetch/test_update_an.py +++ b/repondeur/tests/fetch/test_update_an.py @@ -66,7 +66,7 @@ def test_position_changed(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - assert [amdt.num for amdt in lecture_an.amendements] == [177, 270] + assert [amdt.num for amdt in lecture_an.amendements] == ["177", "270"] assert [amdt.position for amdt in lecture_an.amendements] == [1, 2] with setup_mock_responses( @@ -105,7 +105,7 @@ def test_position_changed(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - assert [amdt.num for amdt in lecture_an.amendements] == [177, 270] + assert [amdt.num for amdt in lecture_an.amendements] == ["177", "270"] assert [amdt.position for amdt in lecture_an.amendements] == [2, 1] @@ -155,7 +155,7 @@ def test_abandoned_before_seance(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - assert [amdt.num for amdt in lecture_an.amendements] == [177, 270] + assert [amdt.num for amdt in lecture_an.amendements] == ["177", "270"] assert [amdt.position for amdt in lecture_an.amendements] == [1, 2] with setup_mock_responses( @@ -186,7 +186,7 @@ def test_abandoned_before_seance(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - assert [amdt.num for amdt in lecture_an.amendements] == [177, 270] + assert [amdt.num for amdt in lecture_an.amendements] == ["177", "270"] assert [amdt.position for amdt in lecture_an.amendements] == [None, 1] @@ -236,7 +236,7 @@ def test_article_changed(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 177).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "177").one() assert str(amendement.article) == "Art. 3" # Fetch updates @@ -302,7 +302,7 @@ def test_article_changed(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 177).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "177").one() assert str(amendement.article) == "Art. 4" @@ -352,7 +352,7 @@ def test_add_parent_amendement(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 270).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "270").one() assert amendement.parent is None # Fetch updates @@ -398,9 +398,9 @@ def test_add_parent_amendement(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 270).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "270").one() assert amendement.parent is not None - assert amendement.parent.num == 177 + assert amendement.parent.num == "177" @responses.activate @@ -455,9 +455,9 @@ def test_remove_parent_amendement(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 270).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "270").one() assert amendement.parent is not None - assert amendement.parent.num == 177 + assert amendement.parent.num == "177" # Fetch updates with setup_mock_responses( @@ -496,7 +496,7 @@ def test_remove_parent_amendement(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 270).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "270").one() assert amendement.parent is None @@ -544,7 +544,7 @@ def test_rectif(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 177).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "177").one() assert amendement.rectif == 0 # Fetch updates @@ -590,7 +590,7 @@ def test_rectif(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 177).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "177").one() assert amendement.rectif == 2 event = next(e for e in amendement.events if isinstance(e, AmendementRectifie)) @@ -642,7 +642,7 @@ def test_rectif_with_nil(lecture_an, source_an): ): source_an.fetch(lecture=lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 177).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "177").one() assert amendement.rectif == 0 # Fetch updates @@ -693,7 +693,7 @@ def test_rectif_with_nil(lecture_an, source_an): assert result.errored == set() - amendement = DBSession.query(Amendement).filter(Amendement.num == 177).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "177").one() assert amendement.rectif == 0 with pytest.raises(StopIteration): diff --git a/repondeur/tests/fetch/test_update_senat.py b/repondeur/tests/fetch/test_update_senat.py index 6bb4ee164..31069c5e1 100644 --- a/repondeur/tests/fetch/test_update_senat.py +++ b/repondeur/tests/fetch/test_update_senat.py @@ -51,15 +51,15 @@ def test_position_changed(lecture_senat, source_senat): source_senat.fetch(lecture_senat) assert {amdt.num: amdt.position for amdt in lecture_senat.amendements} == { - 31: 1, - 443: 2, + "31": 1, + "443": 2, } source_senat.fetch(lecture_senat) assert {amdt.num: amdt.position for amdt in lecture_senat.amendements} == { - 31: 2, - 443: 1, + "31": 2, + "443": 1, } @@ -102,15 +102,15 @@ def test_abandoned_before_seance(lecture_senat, source_senat): source_senat.fetch(lecture_senat) assert {amdt.num: amdt.position for amdt in lecture_senat.amendements} == { - 31: 1, - 443: 2, + "31": 1, + "443": 2, } source_senat.fetch(lecture_senat) assert {amdt.num: amdt.position for amdt in lecture_senat.amendements} == { - 31: 1, - 443: None, + "31": 1, + "443": None, } @@ -158,15 +158,15 @@ def test_article_changed(lecture_senat, source_senat): source_senat.fetch(lecture_senat) assert {amdt.num: str(amdt.article) for amdt in lecture_senat.amendements} == { - 31: "Art. 3", - 443: "Art. 4", + "31": "Art. 3", + "443": "Art. 4", } source_senat.fetch(lecture_senat) assert {amdt.num: str(amdt.article) for amdt in lecture_senat.amendements} == { - 31: "Art. 3", - 443: "Art. 3", + "31": "Art. 3", + "443": "Art. 3", } @@ -210,14 +210,14 @@ def test_add_parent_amendement(lecture_senat, source_senat): assert { amdt.num: amdt.parent.num if amdt.parent else None for amdt in lecture_senat.amendements - } == {31: None, 443: None} + } == {"31": None, "443": None} source_senat.fetch(lecture_senat) assert { amdt.num: amdt.parent.num if amdt.parent else None for amdt in lecture_senat.amendements - } == {31: None, 443: 31} + } == {"31": None, "443": "31"} @responses.activate @@ -260,11 +260,11 @@ def test_remove_parent_amendement(lecture_senat, source_senat): assert { amdt.num: amdt.parent.num if amdt.parent else None for amdt in lecture_senat.amendements - } == {31: None, 443: 31} + } == {"31": None, "443": "31"} source_senat.fetch(lecture_senat) assert { amdt.num: amdt.parent.num if amdt.parent else None for amdt in lecture_senat.amendements - } == {31: None, 443: None} + } == {"31": None, "443": None} diff --git a/repondeur/tests/fixtures/lectures.py b/repondeur/tests/fixtures/lectures.py index acfe798bf..bd3f4d871 100644 --- a/repondeur/tests/fixtures/lectures.py +++ b/repondeur/tests/fixtures/lectures.py @@ -136,7 +136,7 @@ def amendements_an(db, lecture_an, article1_an): Amendement.create( lecture=lecture_an, article=article1_an, num=num, position=position ) - for position, num in enumerate((666, 999), 1) + for position, num in enumerate(("666", "999"), 1) ] DBSession.add_all(amendements) @@ -168,7 +168,7 @@ def amendements_senat(db, lecture_senat, article1_senat): num=num, position=position, ) - for position, num in enumerate((6666, 9999), 1) + for position, num in enumerate(("6666", "9999"), 1) ] DBSession.add_all(amendements) diff --git a/repondeur/tests/fixtures/plf2018.py b/repondeur/tests/fixtures/plf2018.py index 32d951c4d..8afce5811 100644 --- a/repondeur/tests/fixtures/plf2018.py +++ b/repondeur/tests/fixtures/plf2018.py @@ -187,7 +187,7 @@ def amendements_plf2018_an_premiere_lecture_seance_publique_2( mission_titre="Mission Action et transformation publiques", mission_titre_court="Action transfo.", ) - for position, num in enumerate((111, 333), 1) + for position, num in enumerate(("111", "333"), 1) ] DBSession.add_all(amendements) diff --git a/repondeur/tests/integration/test_amendements_filtering.py b/repondeur/tests/integration/test_amendements_filtering.py index 2351d4f07..7d1406943 100644 --- a/repondeur/tests/integration/test_amendements_filtering.py +++ b/repondeur/tests/integration/test_amendements_filtering.py @@ -23,7 +23,7 @@ def test_number_of_amendements_is_displayed( wsgi_server, driver, lecture_an_url, amendements_an ): driver.get(f"{lecture_an_url}/amendements/") - trs = driver.find_elements_by_css_selector("tbody tr") + trs = driver.find_elements_by_css_selector("tbody tr:not(.dropzone)") assert len(trs) == 2 counter = driver.find_element_by_css_selector( 'span[data-target="amendements-filters.count"]' @@ -46,10 +46,10 @@ def test_number_of_amendements_is_displayed_too_many_amendements( with transaction.manager: for i in range(nb_amendements): - Amendement.create(lecture=lecture_an, article=article1_an, num=i + 1) + Amendement.create(lecture=lecture_an, article=article1_an, num=str(i + 1)) driver.get(f"{lecture_an_url}/amendements/") - trs = driver.find_elements_by_css_selector("tbody tr") + trs = driver.find_elements_by_css_selector("tbody tr:not(.dropzone)") assert len(trs) == 7 counter = driver.find_element_by_css_selector( 'span[data-target="amendements-filters.count"]' @@ -69,7 +69,7 @@ def test_number_of_amendements_is_displayed_with_limit_derouleur( DBSession.add_all(amendements_an) driver.get(f"{lecture_an_url}/amendements/") - trs = driver.find_elements_by_css_selector("tbody tr") + trs = driver.find_elements_by_css_selector("tbody tr:not(.dropzone)") assert len(trs) == 3 counter = driver.find_element_by_css_selector( 'span[data-target="amendements-filters.count"]' @@ -80,7 +80,8 @@ def test_number_of_amendements_is_displayed_with_limit_derouleur( @pytest.mark.parametrize( "column_index,input_text,kind,initial,filtered", [ - ("3", "777", "amendement", ["666", "999", "777"], ["777"]), + ("3", "66", "amendement", ["666", "999", "661"], ["666", "661"]), + ("3", "666", "amendement", ["666", "999", "661"], ["666"]), ("4", "Da", "table", ["Ronan", "David", "Daniel"], ["David", "Daniel"]), ], ) @@ -110,18 +111,22 @@ def test_column_filtering_by_value( user_ronan_table_an.add_amendement(amendements_an[0]) user_david_table_an.add_amendement(amendements_an[1]) amendement = Amendement.create( - lecture=lecture_an, article=article1_an, num=777, position=3 + lecture=lecture_an, article=article1_an, num="661", position=3 ) user_daniel_table_an.add_amendement(amendement) driver.get(f"{lecture_an_url}/amendements/") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(input_text) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered assert ( driver.current_url @@ -130,19 +135,25 @@ def test_column_filtering_by_value( # Restore initial state. input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements/" # Check filters are active on URL (re)load. driver.get(f"{lecture_an_url}/amendements/?{kind}={input_text}") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements/" @@ -185,13 +196,17 @@ def test_column_filtering_by_value_when_empty_results( user_daniel_table_an.add_amendement(amendement) driver.get(f"{lecture_an_url}/amendements/") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(input_text) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered empty_message = driver.find_element_by_css_selector( '[data-target="amendements-filters.empty"]' @@ -235,13 +250,17 @@ def test_column_filtering_by_value_with_shared_tables( shared_table_lecture_an.add_amendement(amendements_an[1]) driver.get(f"{lecture_an_url}/amendements") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(input_text) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered assert ( driver.current_url @@ -250,19 +269,25 @@ def test_column_filtering_by_value_with_shared_tables( # Restore initial state. input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements" # Check filters are active on URL (re)load. driver.get(f"{lecture_an_url}/amendements?{kind}={input_text}") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements" @@ -305,36 +330,46 @@ def test_column_filtering_by_value_with_batches( user_ronan_table_an.add_amendement(amendements_an[0]) user_david_table_an.add_amendement(amendements_an[1]) amendement = Amendement.create( - lecture=lecture_an, article=article1_an, num=777, position=3 + lecture=lecture_an, article=article1_an, num="777", position=3 ) user_daniel_table_an.add_amendement(amendement) driver.get(f"{lecture_an_url}/amendements/") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(input_text) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered assert driver.current_url == f"{lecture_an_url}/amendements/?{kind}={input_text}" # Restore initial state. input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements/" # Check filters are active on URL (re)load. driver.get(f"{lecture_an_url}/amendements/?{kind}={input_text}") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements/" @@ -367,38 +402,48 @@ def test_column_filtering_by_checkbox( amendement = Amendement.create( lecture=lecture_an, article=article1_an, - num=777, + num="777", position=3, auteur="LE GOUVERNEMENT", ) user_david_table_an.add_amendement(amendement) driver.get(f"{lecture_an_url}/amendements/") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial label = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) label[for='{kind}']" ) label.click() - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered assert driver.current_url == f"{lecture_an_url}/amendements/?{kind}=1" # Restore initial state. label.click() - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements/" # Check filters are active on URL (re)load. driver.get(f"{lecture_an_url}/amendements/?{kind}=1") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered label = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) label[for='{kind}']" ) label.click() - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{lecture_an_url}/amendements/" @@ -448,7 +493,7 @@ def test_column_filtering_by_value_for_missions( amendement = Amendement.create( lecture=lecture_plf2018_an_premiere_lecture_seance_publique_2, article=article1_plf2018_an_premiere_lecture_seance_publique_2, - num=222, + num="222", position=3, mission_titre="Mission Action extérieure de l'État", mission_titre_court="Action ext.", @@ -456,13 +501,17 @@ def test_column_filtering_by_value_for_missions( DBSession.add(amendement) driver.get(f"{LECTURE_URL}/amendements/") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(input_text) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered assert ( driver.current_url @@ -471,18 +520,24 @@ def test_column_filtering_by_value_for_missions( # Restore initial state. input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{LECTURE_URL}/amendements/" # Check filters are active on URL (re)load. driver.get(f"{LECTURE_URL}/amendements/?{kind}={input_text}") - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == filtered input_field = driver.find_element_by_css_selector( f"thead tr.filters th:nth-child({column_index}) input" ) input_field.send_keys(Keys.BACKSPACE * len(input_text)) - trs = driver.find_elements_by_css_selector(f"tbody tr:not(.hidden-{kind})") + trs = driver.find_elements_by_css_selector( + f"tbody tr:not(.hidden-{kind}):not(.dropzone)" + ) assert extract_column_text(column_index, trs) == initial assert driver.current_url == f"{LECTURE_URL}/amendements/" diff --git a/repondeur/tests/integration/test_amendements_group_actions.py b/repondeur/tests/integration/test_amendements_group_actions.py index db9316193..3aafe3e50 100644 --- a/repondeur/tests/integration/test_amendements_group_actions.py +++ b/repondeur/tests/integration/test_amendements_group_actions.py @@ -117,7 +117,7 @@ def test_group_actions_button_urls_change_with_selection_too_many_amendements( with transaction.manager: for i in range(nb_amendements): - Amendement.create(lecture=lecture_an, article=article1_an, num=i + 1) + Amendement.create(lecture=lecture_an, article=article1_an, num=str(i + 1)) driver.get(f"{lecture_an_url}/amendements/") find = driver.find_element_by_css_selector diff --git a/repondeur/tests/integration/test_amendements_links.py b/repondeur/tests/integration/test_amendements_links.py index 2c500e4f4..8815fdecb 100644 --- a/repondeur/tests/integration/test_amendements_links.py +++ b/repondeur/tests/integration/test_amendements_links.py @@ -18,7 +18,7 @@ def test_column_filtering_changes_edit_url_on_the_fly( ) input_field.send_keys("666") assert driver.current_url == f"{lecture_an_url}/amendements/?amendement=666" - see_td = driver.find_element_by_css_selector("td:nth-child(7)") + see_td = driver.find_element_by_css_selector("tr:not(.dropzone) td:nth-child(7)") see_link = see_td.find_element_by_css_selector("a") assert see_link.get_attribute("href") == f"{lecture_an_url}/amendements/666/" see_link.click() diff --git a/repondeur/tests/integration/test_amendements_select_all.py b/repondeur/tests/integration/test_amendements_select_all.py index cf916ad88..45184a609 100644 --- a/repondeur/tests/integration/test_amendements_select_all.py +++ b/repondeur/tests/integration/test_amendements_select_all.py @@ -63,7 +63,9 @@ def test_select_all_checks_only_visible_amendements( user_ronan_table_an.add_amendement(amendements_an[0]) user_david_table_an.add_amendement(amendements_an[1]) - amendement = Amendement.create(lecture=lecture_an, article=article1_an, num=777) + amendement = Amendement.create( + lecture=lecture_an, article=article1_an, num="777" + ) user_daniel_table_an.add_amendement(amendement) driver.get(f"{lecture_an_url}/amendements/") diff --git a/repondeur/tests/integration/test_table_filtering.py b/repondeur/tests/integration/test_table_filtering.py index 5d102a8d5..8c0071e8a 100644 --- a/repondeur/tests/integration/test_table_filtering.py +++ b/repondeur/tests/integration/test_table_filtering.py @@ -89,7 +89,8 @@ def test_filters_are_absent_without_amendements( ["Article 1", "Article 1", "Article 7 bis"], ["Article 7 bis"], ), - ("2", ".numero", "777", "amendement", ["666", "999", "777"], ["777"]), + ("2", ".numero", "66", "amendement", ["666", "999", "661"], ["666", "661"]), + ("2", ".numero", "661", "amendement", ["666", "999", "661"], ["661"]), ], ) def test_column_filtering_by_value( @@ -116,7 +117,7 @@ def test_column_filtering_by_value( user_david_table_an.add_amendement(amendements_an[0]) user_david_table_an.add_amendement(amendements_an[1]) amendement = Amendement.create( - lecture=lecture_an, article=article7bis_an, num=777 + lecture=lecture_an, article=article7bis_an, num="661" ) user_david_table_an.add_amendement(amendement) @@ -185,7 +186,7 @@ def test_column_filtering_by_value_with_batches( user_david_table_an.add_amendement(amendements_an[0]) user_david_table_an.add_amendement(amendements_an[1]) amendement = Amendement.create( - lecture=lecture_an, article=article7bis_an, num=777 + lecture=lecture_an, article=article7bis_an, num="777" ) user_david_table_an.add_amendement(amendement) @@ -239,7 +240,7 @@ def test_column_filtering_by_checkbox( amendement = Amendement.create( lecture=lecture_an, article=article7bis_an, - num=777, + num="777", auteur="LE GOUVERNEMENT", ) user_david_table_an.add_amendement(amendement) diff --git a/repondeur/tests/integration/test_table_group_actions.py b/repondeur/tests/integration/test_table_group_actions.py index 5a4af895d..4593350cd 100644 --- a/repondeur/tests/integration/test_table_group_actions.py +++ b/repondeur/tests/integration/test_table_group_actions.py @@ -91,7 +91,7 @@ def test_batch_amendements_is_hidden_when_selected_amendements_have_different_ar with transaction.manager: amendement = Amendement.create( - lecture=lecture_an, article=article7bis_an, num=777 + lecture=lecture_an, article=article7bis_an, num="777" ) DBSession.add(user_david_table_an) user_david_table_an.add_amendement(amendements_an[0]) @@ -132,7 +132,7 @@ def test_batch_amendements_is_hidden_when_selected_amendements_have_different_mi lecture=lecture_an, article=article1_an, mission_titre=mission2_titre, - num=777, + num="777", ) DBSession.add(user_david_table_an) diff --git a/repondeur/tests/integration/test_table_select_all.py b/repondeur/tests/integration/test_table_select_all.py index f239e7574..88192128d 100644 --- a/repondeur/tests/integration/test_table_select_all.py +++ b/repondeur/tests/integration/test_table_select_all.py @@ -121,7 +121,7 @@ def test_select_all_checks_only_visible_amendements( user_david_table_an.add_amendement(amendements_an[0]) user_david_table_an.add_amendement(amendements_an[1]) amendement = Amendement.create( - lecture=lecture_an, article=article7bis_an, num=777 + lecture=lecture_an, article=article7bis_an, num="777" ) user_david_table_an.add_amendement(amendement) diff --git a/repondeur/tests/models/test_amendement.py b/repondeur/tests/models/test_amendement.py index e5cfeb671..68ba9cee4 100644 --- a/repondeur/tests/models/test_amendement.py +++ b/repondeur/tests/models/test_amendement.py @@ -3,18 +3,18 @@ from sqlalchemy.exc import IntegrityError EXAMPLES = [ - ("", 0, 0, "0"), - ("COM-1", 1, 0, "1"), - ("COM-48 rect.", 48, 1, "48 rect."), - ("CE208", 208, 0, "208"), - ("CE|208", 208, 0, "208"), - ("42", 42, 0, "42"), - ("42 rect.", 42, 1, "42 rect."), - ("42 rect. bis", 42, 2, "42 rect. bis"), - ("42 rect. ter", 42, 3, "42 rect. ter"), - ("42 rect. nonies", 42, 9, "42 rect. nonies"), - ("42 rect. novies", 42, 9, "42 rect. nonies"), - ("42 rect. undecies", 42, 11, "42 rect. undecies"), + ("", "0", 0, "0"), + ("COM-1", "1", 0, "1"), + ("COM-48 rect.", "48", 1, "48 rect."), + ("CE208", "208", 0, "208"), + ("CE|208", "208", 0, "208"), + ("42", "42", 0, "42"), + ("42 rect.", "42", 1, "42 rect."), + ("42 rect. bis", "42", 2, "42 rect. bis"), + ("42 rect. ter", "42", 3, "42 rect. ter"), + ("42 rect. nonies", "42", 9, "42 rect. nonies"), + ("42 rect. novies", "42", 9, "42 rect. nonies"), + ("42 rect. undecies", "42", 11, "42 rect. undecies"), ] @@ -22,7 +22,7 @@ def test_parse_num(text, num, rectif, disp): from zam_repondeur.models import Amendement - assert Amendement.parse_num(text) == (num, rectif) + assert Amendement.parse_num(text) == (int(num), rectif) @pytest.mark.parametrize("text,num,rectif,disp", EXAMPLES) diff --git a/repondeur/tests/models/test_base.py b/repondeur/tests/models/test_base.py index 62ed9c4b4..9af22daba 100644 --- a/repondeur/tests/models/test_base.py +++ b/repondeur/tests/models/test_base.py @@ -34,7 +34,7 @@ def test_article(self, article1_an): def test_amendement(self, amendements_an): expected_repr = ( - "" ) assert repr(amendements_an[0]) == expected_repr diff --git a/repondeur/tests/models/test_batch.py b/repondeur/tests/models/test_batch.py index 25eaf2875..e53a17e4b 100644 --- a/repondeur/tests/models/test_batch.py +++ b/repondeur/tests/models/test_batch.py @@ -16,7 +16,7 @@ def amendements(db, lecture_an, article1_an): Amendement.create( lecture=lecture_an, article=article1_an, num=num, position=position ) - for position, num in enumerate((1, 2, 3, 4), 1) + for position, num in enumerate(("1", "2", "3", "4"), 1) ] @@ -24,20 +24,30 @@ class TestCollapsedBatches: def test_unbatched_amendements_are_left_alone(self, amendements): from zam_repondeur.models.batch import Batch - assert [a.num for a in Batch.collapsed_batches(amendements)] == [1, 2, 3, 4] + assert [a.num for a in Batch.collapsed_batches(amendements)] == [ + "1", + "2", + "3", + "4", + ] def test_batched_amendements_are_grouped(self, amendements): from zam_repondeur.models.batch import Batch amendements[0].location.batch = amendements[2].location.batch = Batch.create() - assert [a.num for a in Batch.collapsed_batches(amendements)] == [1, 2, 4] + assert [a.num for a in Batch.collapsed_batches(amendements)] == ["1", "2", "4"] class TestExpandedBatches: def test_unbatched_amendements_are_left_alone(self, amendements): from zam_repondeur.models.batch import Batch - assert [a.num for a in Batch.expanded_batches(amendements)] == [1, 2, 3, 4] + assert [a.num for a in Batch.expanded_batches(amendements)] == [ + "1", + "2", + "3", + "4", + ] def test_batched_amendements_are_expanded(self, amendements): from zam_repondeur.models.batch import Batch @@ -48,7 +58,7 @@ def test_batched_amendements_are_expanded(self, amendements): for a in Batch.expanded_batches( [amendements[0], amendements[1], amendements[3]] ) - } == {1, 2, 3, 4} + } == {"1", "2", "3", "4"} def partition( @@ -69,7 +79,7 @@ def test_reversibility(lecture_an, article1_an, nb_amendements): from zam_repondeur.models import Amendement, Batch amendements = [ - Amendement.create(lecture=lecture_an, article=article1_an, num=i + 1) + Amendement.create(lecture=lecture_an, article=article1_an, num=str(i + 1)) for i in range(nb_amendements) ] diff --git a/repondeur/tests/models/test_chambre.py b/repondeur/tests/models/test_chambre.py new file mode 100644 index 000000000..ceb41d0e1 --- /dev/null +++ b/repondeur/tests/models/test_chambre.py @@ -0,0 +1,22 @@ +import pytest + +from zam_repondeur.models.chambre import Chambre + + +@pytest.mark.parametrize( + "text,chambre", + [ + ("an", Chambre.AN), + ("senat", Chambre.SENAT), + ("ccfp", Chambre.CCFP), + ("csfpe", Chambre.CSFPE), + ], +) +def test_chambre_from_string(text, chambre): + assert Chambre.from_string(text) == chambre + + +def test_invalid_chambre(): + with pytest.raises(ValueError) as exc_info: + Chambre.from_string("foo") + assert str(exc_info.value) == "Invalid string value 'foo' for Chambre" diff --git a/repondeur/tests/models/test_lecture.py b/repondeur/tests/models/test_lecture.py index 4f5c57a6e..00065e4f6 100644 --- a/repondeur/tests/models/test_lecture.py +++ b/repondeur/tests/models/test_lecture.py @@ -3,10 +3,67 @@ import pytest import transaction +from zam_repondeur.models.chambre import Chambre + # We need data about dossiers, texts and groups pytestmark = pytest.mark.usefixtures("data_repository") +@pytest.fixture +def lecture_ccfp(db): + from zam_repondeur.models import ( + Dossier, + Lecture, + Phase, + Texte, + TypeTexte, + ) + + dossier = Dossier.create( + titre="Projet de décret", slug="projet-decret", an_id="dummy-projet-decret" + ) + texte = Texte.create( + type_=TypeTexte.PROJET, chambre=Chambre.CCFP, numero=1, date_depot=date.today(), + ) + lecture = Lecture.create( + phase=Phase.PREMIERE_LECTURE, + dossier=dossier, + texte=texte, + organe="Assemblée plénière", + titre="Première lecture – Assemblée plénière", + ) + return lecture + + +@pytest.fixture +def lecture_csfpe(db): + from zam_repondeur.models import ( + Dossier, + Lecture, + Phase, + Texte, + TypeTexte, + ) + + dossier = Dossier.create( + titre="Projet de décret", slug="projet-decret", an_id="dummy-projet-decret" + ) + texte = Texte.create( + type_=TypeTexte.PROJET, + chambre=Chambre.CSFPE, + numero=1, + date_depot=date.today(), + ) + lecture = Lecture.create( + phase=Phase.PREMIERE_LECTURE, + dossier=dossier, + texte=texte, + organe="Assemblée plénière", + titre="Première lecture – Assemblée plénière", + ) + return lecture + + class TestLectureToStr: def test_an_seance_publique_15e_legislature( self, dossier_plfss2018, texte_plfss2018_an_premiere_lecture @@ -117,6 +174,56 @@ def test_senat_seance_publique( ) assert str(lecture) == result + def test_ccfp(self, lecture_ccfp): + assert str(lecture_ccfp) == ( + "Conseil commun de la fonction publique," + " Assemblée plénière, Première lecture" + ) + + def test_csfpe(self, lecture_csfpe): + assert str(lecture_csfpe) == ( + "Conseil supérieur de la fonction publique d’État," + " Assemblée plénière, Première lecture" + ) + + +class TestLectureGet: + def test_get_ccfp(self, lecture_ccfp): + from zam_repondeur.models import Lecture + from zam_repondeur.models import DBSession + + DBSession.flush() + + assert ( + Lecture.get( + dossier=lecture_ccfp.dossier, + chambre=Chambre.CCFP, + organe="Assemblée plénière", + num_texte=lecture_ccfp.texte.numero, + session_or_legislature=None, + partie=None, + ) + is not None + ) + + def test_get_csfpe(self, lecture_csfpe): + from zam_repondeur.models import Lecture + from zam_repondeur.models import DBSession + + DBSession.flush() + + assert ( + Lecture.get( + dossier=lecture_csfpe.dossier, + chambre=Chambre.CSFPE, + organe="Assemblée plénière", + num_texte=lecture_csfpe.texte.numero, + session_or_legislature=None, + partie=None, + ) + is not None + ) + class TestSimilairesMap: def test_empty(self, lecture_an): @@ -131,8 +238,8 @@ def test_no_reponses(self, lecture_an, amendements_an): DBSession.add(lecture_an) amdt_666, amdt_999 = amendements_an assert lecture_an.all_amendements.similaires_map == { - 666: {amdt_666, amdt_999}, - 999: {amdt_666, amdt_999}, + "666": {amdt_666, amdt_999}, + "999": {amdt_666, amdt_999}, } def test_same_reponses(self, lecture_an, amendements_an): @@ -144,14 +251,14 @@ def test_same_reponses(self, lecture_an, amendements_an): DBSession.add_all(amendements_an) lecture_an = DBSession.query(Lecture).one() - amdt_666 = DBSession.query(Amendement).filter_by(num=666).one() - amdt_999 = DBSession.query(Amendement).filter_by(num=999).one() + amdt_666 = DBSession.query(Amendement).filter_by(num="666").one() + amdt_999 = DBSession.query(Amendement).filter_by(num="999").one() assert amdt_666.user_content.reponse_hash == amdt_999.user_content.reponse_hash assert lecture_an.all_amendements.similaires_map == { - 666: {amdt_666, amdt_999}, - 999: {amdt_666, amdt_999}, + "666": {amdt_666, amdt_999}, + "999": {amdt_666, amdt_999}, } def test_different_reponses(self, lecture_an, amendements_an): @@ -163,12 +270,12 @@ def test_different_reponses(self, lecture_an, amendements_an): DBSession.add_all(amendements_an) lecture_an = DBSession.query(Lecture).one() - amdt_666 = DBSession.query(Amendement).filter_by(num=666).one() - amdt_999 = DBSession.query(Amendement).filter_by(num=999).one() + amdt_666 = DBSession.query(Amendement).filter_by(num="666").one() + amdt_999 = DBSession.query(Amendement).filter_by(num="999").one() assert amdt_666.user_content.reponse_hash != amdt_999.user_content.reponse_hash assert lecture_an.all_amendements.similaires_map == { - 666: {amdt_666}, - 999: {amdt_999}, + "666": {amdt_666}, + "999": {amdt_999}, } diff --git a/repondeur/tests/services/import_export/test_export_json.py b/repondeur/tests/services/import_export/test_export_json.py index 4676e243e..2a476adf8 100644 --- a/repondeur/tests/services/import_export/test_export_json.py +++ b/repondeur/tests/services/import_export/test_export_json.py @@ -19,7 +19,7 @@ def test_export_json( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -34,7 +34,7 @@ def test_export_json( lecture=lecture_senat, article=article1av_senat, alinea="", - num=57, + num="57", auteur="M. DURAND", groupe="Les Républicains", matricule="000001", @@ -45,7 +45,7 @@ def test_export_json( lecture=lecture_senat, article=article7bis_senat, alinea="", - num=21, + num="21", auteur="M. MARTIN", groupe=None, matricule="000002", @@ -56,7 +56,7 @@ def test_export_json( lecture=lecture_senat, article=article1_senat, alinea="", - num=43, + num="43", auteur="M. JEAN", groupe="Les Indépendants", matricule="000003", @@ -67,7 +67,7 @@ def test_export_json( lecture=lecture_senat, article=article1_senat, alinea="", - num=596, + num="596", rectif=1, parent=amendement, auteur="M. JEAN", @@ -103,7 +103,7 @@ def test_export_json( "article": "Article 1", "article_titre": "Titre art. 1 Sénat", "parent": "", - "num": 42, + "num": "42", "objet": "", "organe": "PO78718", "position": 1, @@ -148,7 +148,7 @@ def test_export_json_full(lecture_senat, article1_senat, tmpdir): lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -189,7 +189,7 @@ def test_export_json_full(lecture_senat, article1_senat, tmpdir): "article": "Article 1", "article_titre": "", "parent": "", - "num": 42, + "num": "42", "objet": "Un objet", "organe": "PO78718", "position": 1, @@ -221,7 +221,7 @@ def test_write_with_affectation( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -258,7 +258,7 @@ def test_write_with_affectation_box( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -294,7 +294,7 @@ def test_export_json_sous_amendement( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -307,7 +307,7 @@ def test_export_json_sous_amendement( lecture=lecture_senat, article=article1av_senat, alinea="", - num=57, + num="57", auteur="M. DURAND", groupe="Les Républicains", matricule="000001", @@ -318,7 +318,7 @@ def test_export_json_sous_amendement( lecture=lecture_senat, article=article7bis_senat, alinea="", - num=21, + num="21", auteur="M. MARTIN", groupe=None, matricule="000002", @@ -329,7 +329,7 @@ def test_export_json_sous_amendement( lecture=lecture_senat, article=article1_senat, alinea="", - num=43, + num="43", auteur="M. JEAN", groupe="Les Indépendants", matricule="000003", @@ -340,7 +340,7 @@ def test_export_json_sous_amendement( lecture=lecture_senat, article=article1_senat, alinea="", - num=596, + num="596", rectif=1, parent=amendement, auteur="M. JEAN", @@ -377,7 +377,7 @@ def test_export_json_sous_amendement( "article": "Article 1", "article_titre": "", "parent": "42 rect.", - "num": 596, + "num": "596", "objet": "", "organe": "PO78718", "position": "", diff --git a/repondeur/tests/services/import_export/test_export_pdf.py b/repondeur/tests/services/import_export/test_export_pdf.py index 4c70132e2..b248a445f 100644 --- a/repondeur/tests/services/import_export/test_export_pdf.py +++ b/repondeur/tests/services/import_export/test_export_pdf.py @@ -26,7 +26,7 @@ def test_generate_pdf_without_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -40,7 +40,7 @@ def test_generate_pdf_without_responses( lecture=lecture_senat, article=article1av_senat, alinea="", - num=57, + num="57", auteur="M. DURAND", groupe="Les Républicains", matricule="000001", @@ -51,7 +51,7 @@ def test_generate_pdf_without_responses( lecture=lecture_senat, article=article7bis_senat, alinea="", - num=21, + num="21", auteur="M. MARTIN", groupe=None, matricule="000002", @@ -62,7 +62,7 @@ def test_generate_pdf_without_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=43, + num="43", auteur="M. JEAN", groupe="Les Indépendants", matricule="000003", @@ -73,7 +73,7 @@ def test_generate_pdf_without_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=596, + num="596", rectif=1, parent=amendement, auteur="M. JEAN", @@ -107,7 +107,7 @@ def test_generate_pdf_with_amendement_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -122,7 +122,7 @@ def test_generate_pdf_with_amendement_responses( lecture=lecture_senat, article=article1av_senat, alinea="", - num=57, + num="57", auteur="M. DURAND", groupe="Les Républicains", matricule="000001", @@ -133,7 +133,7 @@ def test_generate_pdf_with_amendement_responses( lecture=lecture_senat, article=article7bis_senat, alinea="", - num=21, + num="21", auteur="M. MARTIN", groupe=None, matricule="000002", @@ -144,7 +144,7 @@ def test_generate_pdf_with_amendement_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=43, + num="43", auteur="M. JEAN", groupe="Les Indépendants", matricule="000003", @@ -155,7 +155,7 @@ def test_generate_pdf_with_amendement_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=596, + num="596", rectif=1, parent=amendement, auteur="M. JEAN", @@ -366,7 +366,7 @@ def test_generate_pdf_with_amendement_content_factor_many_authors_groups( Amendement.create( lecture=lecture_senat, article=article1_senat, - num=42, + num="42", position=3, auteur="M. DUPONT", groupe="RDSE", @@ -378,7 +378,7 @@ def test_generate_pdf_with_amendement_content_factor_many_authors_groups( Amendement.create( lecture=lecture_senat, article=article1_senat, - num=57, + num="57", position=4, auteur="M. DURAND", groupe="Les Républicains", @@ -390,7 +390,7 @@ def test_generate_pdf_with_amendement_content_factor_many_authors_groups( Amendement.create( lecture=lecture_senat, article=article1_senat, - num=72, + num="72", position=5, auteur="M. MARTIN", groupe="Les Républicains", @@ -402,7 +402,7 @@ def test_generate_pdf_with_amendement_content_factor_many_authors_groups( Amendement.create( lecture=lecture_senat, article=article1_senat, - num=83, + num="83", position=6, auteur="M. MARTIN", groupe="Les Républicains", @@ -495,7 +495,7 @@ def test_generate_pdf_with_amendement_and_sous_amendement_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -510,7 +510,7 @@ def test_generate_pdf_with_amendement_and_sous_amendement_responses( lecture=lecture_senat, article=article1av_senat, alinea="", - num=57, + num="57", auteur="M. DURAND", groupe="Les Républicains", matricule="000001", @@ -521,7 +521,7 @@ def test_generate_pdf_with_amendement_and_sous_amendement_responses( lecture=lecture_senat, article=article7bis_senat, alinea="", - num=21, + num="21", auteur="M. MARTIN", groupe=None, matricule="000002", @@ -532,7 +532,7 @@ def test_generate_pdf_with_amendement_and_sous_amendement_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=43, + num="43", auteur="M. JEAN", groupe="Les Indépendants", matricule="000003", @@ -543,7 +543,7 @@ def test_generate_pdf_with_amendement_and_sous_amendement_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=596, + num="596", rectif=1, parent=amendement, auteur="M. JEAN", @@ -584,7 +584,7 @@ def test_generate_pdf_with_additional_article_amendements_having_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -599,7 +599,7 @@ def test_generate_pdf_with_additional_article_amendements_having_responses( lecture=lecture_senat, article=article1av_senat, alinea="", - num=57, + num="57", auteur="M. DURAND", groupe="Les Républicains", matricule="000001", @@ -611,7 +611,7 @@ def test_generate_pdf_with_additional_article_amendements_having_responses( lecture=lecture_senat, article=article7bis_senat, alinea="", - num=21, + num="21", auteur="M. MARTIN", groupe=None, matricule="000002", @@ -622,7 +622,7 @@ def test_generate_pdf_with_additional_article_amendements_having_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=43, + num="43", auteur="M. JEAN", groupe="Les Indépendants", matricule="000003", @@ -633,7 +633,7 @@ def test_generate_pdf_with_additional_article_amendements_having_responses( lecture=lecture_senat, article=article1_senat, alinea="", - num=596, + num="596", rectif=1, parent=amendement, auteur="M. JEAN", @@ -671,7 +671,7 @@ def test_generate_pdf_amendement_without_responses(app, lecture_senat, article1_ lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -703,7 +703,7 @@ def test_generate_pdf_amendement_with_responses(app, lecture_senat, article1_sen lecture=lecture_senat, article=article1_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -853,7 +853,7 @@ def test_generate_pdf_amendement_with_similaire_different_articles( lecture=lecture_senat, article=article7bis_senat, alinea="", - num=42, + num="42", rectif=1, auteur="M. DUPONT", groupe="RDSE", @@ -943,7 +943,7 @@ def another_amendements_an_batch(lecture_an, article1_an): position=position, batch=batch, ) - for position, num in enumerate((555, 888), 3) + for position, num in enumerate(("555", "888"), 3) ] DBSession.add_all(amendements) diff --git a/repondeur/tests/services/import_export/test_export_spreadsheet.py b/repondeur/tests/services/import_export/test_export_spreadsheet.py index 3767560d9..a4797be11 100644 --- a/repondeur/tests/services/import_export/test_export_spreadsheet.py +++ b/repondeur/tests/services/import_export/test_export_spreadsheet.py @@ -106,7 +106,7 @@ def test_export_csv_with_parent(lecture_an, article1_an, tmpdir): reponse="Une réponse très appropriée", comments="Avec des commentaires", ) - for position, num in enumerate((333, 777), 1) + for position, num in enumerate(("333", "777"), 1) ] amendements[1].parent = amendements[0] DBSession.add_all(amendements) @@ -141,7 +141,7 @@ def test_export_csv_with_auteur(lecture_an, article1_an, tmpdir): reponse="Une réponse très appropriée", comments="Avec des commentaires", ) - for position, num in enumerate((333, 777), 1) + for position, num in enumerate(("333", "777"), 1) ] amendements[0].auteur = "Victor Hugo" DBSession.add_all(amendements) @@ -176,7 +176,7 @@ def test_export_csv_with_gouvernemental(lecture_an, article1_an, tmpdir): reponse="Une réponse très appropriée", comments="Avec des commentaires", ) - for position, num in enumerate((333, 777), 1) + for position, num in enumerate(("333", "777"), 1) ] amendements[0].gouvernemental = True DBSession.add_all(amendements) @@ -211,7 +211,7 @@ def test_export_csv_with_identique(lecture_an, article1_an, tmpdir): reponse="Une réponse très appropriée", comments="Avec des commentaires", ) - for position, num in enumerate((333, 444, 777), 1) + for position, num in enumerate(("333", "444", "777"), 1) ] amendements[0].id_identique = 42 amendements[1].id_identique = 42 diff --git a/repondeur/tests/services/import_export/test_liasse_xml.py b/repondeur/tests/services/import_export/test_liasse_xml.py index be09f8e76..631e7f331 100644 --- a/repondeur/tests/services/import_export/test_liasse_xml.py +++ b/repondeur/tests/services/import_export/test_liasse_xml.py @@ -160,7 +160,7 @@ def test_import_liasse_xml_with_known_but_missing_parent( lecture_essoc2018_an_nouvelle_lecture_commission_fond, ) assert len(amendements) == 1 - assert amendements[0].num == 28 + assert amendements[0].num == "28" assert amendements[0].parent is None assert errors == [] @@ -170,8 +170,8 @@ def test_import_liasse_xml_with_known_but_missing_parent( lecture_essoc2018_an_nouvelle_lecture_commission_fond, ) assert len(amendements) == 1 - assert amendements[0].num == 26 - assert amendements[0].parent.num == 28 + assert amendements[0].num == "26" + assert amendements[0].parent.num == "28" assert errors == [] @@ -254,7 +254,7 @@ def _check_amendement_0(amendement): assert amendement.alinea == "24" - assert amendement.num == 28 + assert amendement.num == "28" assert amendement.rectif == 0 assert amendement.auteur == "Fabrice Brun" @@ -302,7 +302,7 @@ def _check_amendement_1(amendement): assert amendement.alinea == "12" - assert amendement.num == 26 + assert amendement.num == "26" assert amendement.rectif == 0 assert amendement.auteur == "Fabrice Brun" @@ -317,7 +317,7 @@ def _check_amendement_1(amendement): assert amendement.id_discussion_commune is None assert amendement.id_identique is None - assert amendement.parent.num == 28 + assert amendement.parent.num == "28" assert amendement.parent.rectif == 0 assert amendement.corps == ( diff --git a/repondeur/tests/services/import_export/test_update_liasse_xml.py b/repondeur/tests/services/import_export/test_update_liasse_xml.py index c0da0452e..a20d130e3 100644 --- a/repondeur/tests/services/import_export/test_update_liasse_xml.py +++ b/repondeur/tests/services/import_export/test_update_liasse_xml.py @@ -45,7 +45,7 @@ def test_add_parent_amendement(lecture_essoc2018_an_nouvelle_lecture_commission_ amendements2, errors = import_liasse_xml( open_liasse("liasse.xml"), lecture_essoc2018_an_nouvelle_lecture_commission_fond ) - assert amendements[1].parent.num == amendements2[1].parent.num == 28 + assert amendements[1].parent.num == amendements2[1].parent.num == "28" assert errors == [] @@ -58,7 +58,7 @@ def test_remove_parent_amendement( amendements, _ = import_liasse_xml( open_liasse("liasse.xml"), lecture_essoc2018_an_nouvelle_lecture_commission_fond ) - assert amendements[1].parent.num == 28 + assert amendements[1].parent.num == "28" # And import a liasse without the parent amendements2, errors = import_liasse_xml( diff --git a/repondeur/tests/test_utils.py b/repondeur/tests/test_utils.py index 9b8431bd0..7f1b1a15d 100644 --- a/repondeur/tests/test_utils.py +++ b/repondeur/tests/test_utils.py @@ -1,7 +1,9 @@ import pytest -@pytest.mark.parametrize("input,output", [("1", 1), ("1\nfoo", 1), ("1,\nbar", 1)]) +@pytest.mark.parametrize( + "input,output", [("1", "1"), ("1\nfoo", "1"), ("1,\nbar", "1")] +) def test_normalize_num(input, output): from zam_repondeur.utils import normalize_num diff --git a/repondeur/tests/testapp.py b/repondeur/tests/testapp.py index 54a3c8c35..47ac22e77 100644 --- a/repondeur/tests/testapp.py +++ b/repondeur/tests/testapp.py @@ -1,3 +1,5 @@ +from contextlib import contextmanager + from pyramid.decorator import reify from selectolax.parser import HTMLParser from webtest import TestApp as BaseTestApp @@ -40,3 +42,35 @@ class TestRequest(BaseTestRequest): class TestApp(BaseTestApp): RequestClass = TestRequest + + def get(self, *args, **kwargs): + with self.auto_login(kwargs): + return super().get(*args, **kwargs) + + def post(self, *args, **kwargs): + with self.auto_login(kwargs): + return super().post(*args, **kwargs) + + def post_json(self, *args, **kwargs): + with self.auto_login(kwargs): + return super().post_json(*args, **kwargs) + + @contextmanager + def auto_login(self, kwargs): + from zam_repondeur.models import User + + user = kwargs.pop("user", None) + if user is not None: + assert isinstance(user, User) + self.user_login(email=user.email, headers=kwargs.get("headers")) + + yield + + def user_login(self, email, headers=None): + from zam_repondeur.auth import generate_auth_token + from zam_repondeur.services.users import repository + + token = generate_auth_token() + repository.set_auth_token(email, token) + resp = self.get("/authentification", params={"token": token}, headers=headers) + assert resp.status_code == 302 diff --git a/repondeur/tests/views/test_amendement_edit.py b/repondeur/tests/views/test_amendement_edit.py index 123463bab..acf848520 100644 --- a/repondeur/tests/views/test_amendement_edit.py +++ b/repondeur/tests/views/test_amendement_edit.py @@ -32,7 +32,7 @@ def test_get_amendement_edit_form( # Check the displayed amendement assert resp.parser.css_first(".title .article").text().strip() == "Article 1" - assert resp.parser.css_first(".expose h4").text() == "Exposé" + assert resp.parser.css_first(".expose h4").text() == "Exposé des motifs" assert resp.parser.css_first(".expose h4 + *").text() == "Bla bla bla" assert resp.parser.css_first(".corps h4").text() == "Corps de l’amendement" @@ -207,7 +207,7 @@ def test_post_amendement_edit_form( DBSession.add(user_david_table_an) user_david_table_an.add_amendement(amendement) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None @@ -229,7 +229,7 @@ def test_post_amendement_edit_form( "#amdt-999" ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis == "Favorable" assert amendement.user_content.objet == "Un objet très pertinent" assert ( @@ -266,7 +266,7 @@ def test_post_amendement_edit_form_reset_editing_state( form["comments"] = "Avec des
commentaires
" resp = form.submit("save") - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert not amendement.is_being_edited @@ -311,7 +311,7 @@ def test_post_amendement_edit_form_switch_table( assert "Les modifications n’ont PAS été enregistrées" in resp.text assert "Il est actuellement sur la table de Ronan" in resp.text - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None @@ -331,7 +331,7 @@ def test_post_amendement_edit_form_and_transfer( DBSession.add(user_david_table_an) user_david_table_an.add_amendement(amendement) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None @@ -358,7 +358,7 @@ def test_post_amendement_edit_form_and_transfer( "%2F%23amdt-999" ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis == "Favorable" assert amendement.user_content.objet == "Un objet très pertinent" assert ( @@ -385,7 +385,7 @@ def test_post_amendement_edit_form_gouvernemental( amendement.auteur = "LE GOUVERNEMENT" user_david_table_an.add_amendement(amendement) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None @@ -406,7 +406,7 @@ def test_post_amendement_edit_form_gouvernemental( "#amdt-999" ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert ( diff --git a/repondeur/tests/views/test_amendement_edit_batch.py b/repondeur/tests/views/test_amendement_edit_batch.py index 62f42253d..714bc7bc0 100644 --- a/repondeur/tests/views/test_amendement_edit_batch.py +++ b/repondeur/tests/views/test_amendement_edit_batch.py @@ -32,7 +32,7 @@ def test_get_amendement_edit_form( # Check the displayed amendement assert resp.parser.css_first(".title .article").text().strip() == "Article 1" - assert resp.parser.css_first(".expose h4").text() == "Exposé" + assert resp.parser.css_first(".expose h4").text() == "Exposé des motifs" assert resp.parser.css_first(".expose h4 + *").text() == "Bla bla bla" assert resp.parser.css_first(".corps h4").text() == "Corps de l’amendement" @@ -98,8 +98,8 @@ def test_transfer_amendement_from_edit_form( user_david = DBSession.query(User).filter(User.email == user_david.email).first() table = user_david.table_for(lecture_an) assert len(table.amendements) == 2 - assert table.amendements[0].num == 666 - assert table.amendements[1].num == 999 + assert table.amendements[0].num == "666" + assert table.amendements[1].num == "999" # An event was added to the amendement assert len(amdt.events) == 1 @@ -173,7 +173,7 @@ def test_post_amendement_edit_form_save_batch( DBSession.add(user_david_table_an) user_david_table_an.add_amendement(amendement) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None @@ -195,7 +195,7 @@ def test_post_amendement_edit_form_save_batch( "#amdt-666" ) - amendement_666 = DBSession.query(Amendement).filter(Amendement.num == 666).one() + amendement_666 = DBSession.query(Amendement).filter(Amendement.num == "666").one() assert amendement_666.user_content.avis == "Favorable" assert amendement_666.user_content.objet == "Un objet très pertinent" assert ( @@ -209,7 +209,7 @@ def test_post_amendement_edit_form_save_batch( # Should create events. assert len(amendement_666.events) == 4 - amendement_999 = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement_999 = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement_999.user_content.avis == "Favorable" assert amendement_999.user_content.objet == "Un objet très pertinent" assert ( @@ -248,9 +248,9 @@ def test_post_amendement_edit_form_reset_editing_state( form["comments"] = "Avec des
commentaires
" resp = form.submit("save") - amendement_666 = DBSession.query(Amendement).filter(Amendement.num == 666).one() + amendement_666 = DBSession.query(Amendement).filter(Amendement.num == "666").one() assert not amendement_666.is_being_edited - amendement_999 = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement_999 = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert not amendement_999.is_being_edited @@ -295,7 +295,7 @@ def test_post_amendement_edit_form_switch_table( assert "Les modifications n’ont PAS été enregistrées" in resp.text assert "Il est actuellement sur la table de Ronan" in resp.text - amendement_666 = DBSession.query(Amendement).filter(Amendement.num == 666).one() + amendement_666 = DBSession.query(Amendement).filter(Amendement.num == "666").one() assert amendement_666.user_content.avis is None assert amendement_666.user_content.objet is None assert amendement_666.user_content.reponse is None @@ -303,7 +303,7 @@ def test_post_amendement_edit_form_switch_table( # Should NOT create events. assert len(amendement_666.events) == 0 - amendement_999 = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement_999 = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement_999.user_content.avis is None assert amendement_999.user_content.objet is None assert amendement_999.user_content.reponse is None @@ -322,7 +322,7 @@ def test_post_amendement_edit_form_and_transfer( DBSession.add(user_david_table_an) user_david_table_an.add_amendement(amendement) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None @@ -349,7 +349,7 @@ def test_post_amendement_edit_form_and_transfer( "%2F%23amdt-666" ) - amendement_666 = DBSession.query(Amendement).filter(Amendement.num == 666).one() + amendement_666 = DBSession.query(Amendement).filter(Amendement.num == "666").one() assert amendement_666.user_content.avis == "Favorable" assert amendement_666.user_content.objet == "Un objet très pertinent" assert ( @@ -363,7 +363,7 @@ def test_post_amendement_edit_form_and_transfer( # Should create events. assert len(amendement_666.events) == 4 - amendement_999 = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement_999 = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement_999.user_content.avis == "Favorable" assert amendement_999.user_content.objet == "Un objet très pertinent" assert ( diff --git a/repondeur/tests/views/test_amendements_identiques.py b/repondeur/tests/views/test_amendements_identiques.py index 71cdf4798..23b9511a7 100644 --- a/repondeur/tests/views/test_amendements_identiques.py +++ b/repondeur/tests/views/test_amendements_identiques.py @@ -6,10 +6,10 @@ def test_amendements_not_identiques(app, lecture_an_url, amendements_an, user_da assert resp.status_code == 200 - amendements = resp.parser.css("tbody tr") + amendements = resp.parser.css("tbody tr:not(.dropzone)") assert len(amendements) == 2 - identiques = resp.parser.css("tbody tr td.identique") + identiques = resp.parser.css("tbody tr:not(.dropzone) td.identique") assert len(identiques) == 0 @@ -30,10 +30,10 @@ def test_amendements_identiques(app, lecture_an_url, amendements_an, user_david) assert resp.status_code == 200 - amendements = resp.parser.css("tbody tr") + amendements = resp.parser.css("tbody tr:not(.dropzone)") assert len(amendements) == 2 - identiques = resp.parser.css("tbody tr td.identique") + identiques = resp.parser.css("tbody tr:not(.dropzone) td.identique") assert len(identiques) == 2 assert "first" in identiques[0].attributes["class"] @@ -64,8 +64,8 @@ def test_amendements_identiques_with_abandoned( assert resp.status_code == 200 - amendements = resp.parser.css("tbody tr") + amendements = resp.parser.css("tbody tr:not(.dropzone)") assert len(amendements) == 2 - identiques = resp.parser.css("tbody tr td.identique") + identiques = resp.parser.css("tbody tr:not(.dropzone) td.identique") assert len(identiques) == 0 diff --git a/repondeur/tests/views/test_article_edit.py b/repondeur/tests/views/test_article_edit.py index da13294be..33738371d 100644 --- a/repondeur/tests/views/test_article_edit.py +++ b/repondeur/tests/views/test_article_edit.py @@ -28,7 +28,7 @@ def test_get_article_edit_form_not_found_bad_format(app, lecture_an, user_david) def test_post_article_edit_form_title(app, lecture_an_url, amendements_an, user_david): from zam_repondeur.models import Amendement, DBSession - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "" resp = app.get( @@ -42,7 +42,7 @@ def test_post_article_edit_form_title(app, lecture_an_url, amendements_an, user_ assert resp.status_code == 302 assert resp.location == f"https://zam.test{lecture_an_url}/amendements/" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "Titre article" assert len(amendement.article.events) == 1 @@ -53,7 +53,7 @@ def test_post_article_edit_form_title_cleaned( ): from zam_repondeur.models import Amendement, DBSession - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "" resp = app.get( @@ -67,7 +67,7 @@ def test_post_article_edit_form_title_cleaned( assert resp.status_code == 302 assert resp.location == f"https://zam.test{lecture_an_url}/amendements/" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "Titre article" assert len(amendement.article.events) == 1 @@ -83,7 +83,7 @@ def test_post_article_edit_form_title_redirect_next( DBSession.add(article_2) DBSession.add(lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "" resp = app.get( @@ -100,7 +100,7 @@ def test_post_article_edit_form_title_redirect_next( "/dossiers/plfss-2018/lectures/an.15.269.PO717460/articles/article.2../" ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "Titre article" @@ -116,7 +116,7 @@ def test_post_article_edit_form_title_redirect_amendements_if_intersticial_is_la DBSession.add(article_1_apres) DBSession.add(lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "" resp = app.get( @@ -130,7 +130,7 @@ def test_post_article_edit_form_title_redirect_amendements_if_intersticial_is_la assert resp.status_code == 302 assert resp.location == f"https://zam.test{lecture_an_url}/amendements/" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "Titre article" @@ -148,7 +148,7 @@ def test_post_article_edit_form_title_redirect_next_with_apres( DBSession.add(article_2) DBSession.add(lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "" resp = app.get( @@ -165,7 +165,7 @@ def test_post_article_edit_form_title_redirect_next_with_apres( "/dossiers/plfss-2018/lectures/an.15.269.PO717460/articles/article.2../" ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "Titre article" @@ -187,7 +187,7 @@ def test_post_article_edit_form_title_redirect_next_with_apres_and_avant( DBSession.add(article_2) DBSession.add(lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "" resp = app.get( @@ -204,7 +204,7 @@ def test_post_article_edit_form_title_redirect_next_with_apres_and_avant( "/dossiers/plfss-2018/lectures/an.15.269.PO717460/articles/article.2../" ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.title == "Titre article" @@ -213,7 +213,7 @@ def test_post_article_edit_form_presentation( ): from zam_repondeur.models import Amendement, DBSession - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.presentation == "" resp = app.get( @@ -227,7 +227,7 @@ def test_post_article_edit_form_presentation( assert resp.status_code == 302 assert resp.location == f"https://zam.test{lecture_an_url}/amendements/" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.presentation == "

Content

" assert len(amendement.article.events) == 1 @@ -238,7 +238,7 @@ def test_post_article_edit_form_presentation_cleaned( ): from zam_repondeur.models import Amendement, DBSession - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.presentation == "" resp = app.get( @@ -252,5 +252,5 @@ def test_post_article_edit_form_presentation_cleaned( assert resp.status_code == 302 assert resp.location == f"https://zam.test{lecture_an_url}/amendements/" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert amendement.article.user_content.presentation == "Content" diff --git a/repondeur/tests/views/test_article_events.py b/repondeur/tests/views/test_article_events.py index 9a3b1d2fd..c3dd726b2 100644 --- a/repondeur/tests/views/test_article_events.py +++ b/repondeur/tests/views/test_article_events.py @@ -8,7 +8,7 @@ def test_post_article_edit_form_title(app, lecture_an, amendements_an, user_davi with transaction.manager: DBSession.add(user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() resp = app.get( "/dossiers/plfss-2018/lectures/an.15.269.PO717460/articles/article.1../", @@ -18,7 +18,7 @@ def test_post_article_edit_form_title(app, lecture_an, amendements_an, user_davi form["title"] = "Titre article" resp = form.submit() - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert len(amendement.article.events) == 1 event = amendement.article.events[0] @@ -42,7 +42,7 @@ def test_post_article_edit_form_presentation( with transaction.manager: DBSession.add(user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() resp = app.get( "/dossiers/plfss-2018/lectures/an.15.269.PO717460/articles/article.1../", @@ -52,7 +52,7 @@ def test_post_article_edit_form_presentation( form["presentation"] = "

Content

" resp = form.submit() - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).one() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").one() assert len(amendement.article.events) == 1 event = amendement.article.events[0] diff --git a/repondeur/tests/views/test_auth.py b/repondeur/tests/views/test_auth.py index 6a9a05930..bc7fea3e7 100644 --- a/repondeur/tests/views/test_auth.py +++ b/repondeur/tests/views/test_auth.py @@ -80,7 +80,7 @@ def test_an_email_with_a_token_is_sent_if_address_is_valid( Ce lien contient un code personnel à usage unique, valable 10 minutes, pour vous authentifier sur Zam. - Une fois connecté·e, vous pourrez directement accéder aux dossiers : + Une fois connecté·e, vous pourrez directement accéder à l’application : https://zam.test/dossiers/ Bonne journée !""" diff --git a/repondeur/tests/views/test_authorization.py b/repondeur/tests/views/test_authorization.py index a11d3e5e4..d2e68a807 100644 --- a/repondeur/tests/views/test_authorization.py +++ b/repondeur/tests/views/test_authorization.py @@ -16,4 +16,4 @@ def test_non_team_member_cannot_access_owned_dossier( resp = resp.maybe_follow() - assert "L’accès à ce dossier est réservé aux personnes autorisées." in resp.text + assert "L’accès à cette page est réservé aux personnes autorisées." in resp.text diff --git a/repondeur/tests/views/test_dossiers_add.py b/repondeur/tests/views/test_dossiers_add.py index f7fe9fcbe..e9a473ee7 100644 --- a/repondeur/tests/views/test_dossiers_add.py +++ b/repondeur/tests/views/test_dossiers_add.py @@ -56,7 +56,7 @@ def test_get_form_non_sgg_user(app, user_david): resp = resp.maybe_follow() assert resp.status_code == 200 - assert "L’accès à ce dossier est réservé aux personnes autorisées." in resp.text + assert "L’accès à cette page est réservé aux personnes autorisées." in resp.text def test_get_form_does_not_propose_dossiers_with_teams( @@ -194,6 +194,7 @@ def test_plfss_2018_an(self, app, user_sgg, dossier_plfss2018): ) lecture = Lecture.get( + dossier=dossier_plfss2018, chambre=Chambre.AN, session_or_legislature="15", num_texte=269, @@ -232,7 +233,13 @@ def test_plfss_2018_an(self, app, user_sgg, dossier_plfss2018): } # We should have loaded 5 amendements - assert [amdt.num for amdt in lecture.amendements] == [177, 270, 723, 135, 192] + assert [amdt.num for amdt in lecture.amendements] == [ + "177", + "270", + "723", + "135", + "192", + ] @responses.activate def test_plfss_2018_an_using_fallback(self, app, user_sgg, dossier_plfss2018): @@ -375,6 +382,7 @@ def test_plfss_2018_an_using_fallback(self, app, user_sgg, dossier_plfss2018): ) lecture = Lecture.get( + dossier=dossier_plfss2018, chambre=Chambre.AN, session_or_legislature="15", num_texte=269, @@ -413,7 +421,13 @@ def test_plfss_2018_an_using_fallback(self, app, user_sgg, dossier_plfss2018): } # We should have loaded 5 amendements - assert [amdt.num for amdt in lecture.amendements] == [177, 270, 723, 135, 192] + assert [amdt.num for amdt in lecture.amendements] == [ + "177", + "270", + "723", + "135", + "192", + ] @responses.activate def test_plfss_2019_senat(self, app, user_sgg, dossier_plfss2019): @@ -470,6 +484,7 @@ def test_plfss_2019_senat(self, app, user_sgg, dossier_plfss2019): assert "Dossier créé avec succès," in resp.text lecture = Lecture.get( + dossier=dossier_plfss2019, chambre=Chambre.SENAT, session_or_legislature="2018-2019", num_texte=106, @@ -500,7 +515,7 @@ def test_plfss_2019_senat(self, app, user_sgg, dossier_plfss2019): assert {article.num for article in lecture.articles} == {"1", "19", "29"} # We should have loaded 2 amendements - assert [amdt.num for amdt in lecture.amendements] == [629, 1] + assert [amdt.num for amdt in lecture.amendements] == ["629", "1"] @responses.activate def test_plfss_2018_an_dossier_already_activated( diff --git a/repondeur/tests/views/test_dossiers_invite.py b/repondeur/tests/views/test_dossiers_invite.py index 2d38546e7..d8961505a 100644 --- a/repondeur/tests/views/test_dossiers_invite.py +++ b/repondeur/tests/views/test_dossiers_invite.py @@ -44,7 +44,7 @@ def test_get_form_user_not_in_dossier_team(app, user_ronan, dossier_plfss2018): resp = resp.maybe_follow() assert resp.status_code == 200 - assert "L’accès à ce dossier est réservé aux personnes autorisées." in resp.text + assert "L’accès à cette page est réservé aux personnes autorisées." in resp.text def test_post_form(app, user_david, dossier_plfss2018, mailer): diff --git a/repondeur/tests/views/test_download_amendements.py b/repondeur/tests/views/test_download_amendements.py index bfa2f5ed3..9d6f5bc40 100644 --- a/repondeur/tests/views/test_download_amendements.py +++ b/repondeur/tests/views/test_download_amendements.py @@ -62,7 +62,7 @@ def test_download_pdf_multiple_amendements_multiple_articles( from zam_repondeur.models import Amendement with transaction.manager: - Amendement.create(lecture=lecture_an, article=article7bis_an, num=777) + Amendement.create(lecture=lecture_an, article=article7bis_an, num="777") resp = app.get( ( @@ -85,7 +85,7 @@ def test_download_pdf_lots_of_amendements(app, lecture_an, article1_an, user_dav nb_amendements = 11 with transaction.manager: for i in range(nb_amendements): - Amendement.create(lecture=lecture_an, article=article1_an, num=i + 1) + Amendement.create(lecture=lecture_an, article=article1_an, num=str(i + 1)) params = "&".join(f"n={i+1}" for i in range(nb_amendements)) resp = app.get( @@ -147,7 +147,7 @@ def test_download_xlsx_multiple_amendements_multiple_articles( from zam_repondeur.models import Amendement with transaction.manager: - Amendement.create(lecture=lecture_an, article=article7bis_an, num=777) + Amendement.create(lecture=lecture_an, article=article7bis_an, num="777") resp = app.get( ( @@ -173,7 +173,7 @@ def test_download_xlsx_lots_of_amendements(app, lecture_an, article1_an, user_da nb_amendements = 11 with transaction.manager: for i in range(nb_amendements): - Amendement.create(lecture=lecture_an, article=article1_an, num=i + 1) + Amendement.create(lecture=lecture_an, article=article1_an, num=str(i + 1)) params = "&".join(f"n={i+1}" for i in range(nb_amendements)) resp = app.get( diff --git a/repondeur/tests/views/test_lecture_batch.py b/repondeur/tests/views/test_lecture_batch.py index d1bf33d26..083b19fad 100644 --- a/repondeur/tests/views/test_lecture_batch.py +++ b/repondeur/tests/views/test_lecture_batch.py @@ -740,7 +740,7 @@ def test_lecture_post_batch_reset_amendement( with transaction.manager: amendement_777 = Amendement.create( - lecture=lecture_an, article=article1_an, num=777 + lecture=lecture_an, article=article1_an, num="777" ) user_david_table_an.add_amendement(amendement_777) assert not amendement_777.location.batch @@ -782,9 +782,9 @@ def test_lecture_post_batch_reset_amendement( ) # Reload amendements as they were updated in another transaction. - amendement_666 = Amendement.get(lecture_an, 666) - amendement_999 = Amendement.get(lecture_an, 999) - amendement_777 = Amendement.get(lecture_an, 777) + amendement_666 = Amendement.get(lecture_an, "666") + amendement_999 = Amendement.get(lecture_an, "999") + amendement_777 = Amendement.get(lecture_an, "777") # A new batch is created and 999 is also included. assert amendement_666.location.batch.pk == 2 diff --git a/repondeur/tests/views/test_amendements.py b/repondeur/tests/views/test_lecture_index.py similarity index 89% rename from repondeur/tests/views/test_amendements.py rename to repondeur/tests/views/test_lecture_index.py index ba866a25a..7970579fe 100644 --- a/repondeur/tests/views/test_amendements.py +++ b/repondeur/tests/views/test_lecture_index.py @@ -12,6 +12,8 @@ def test_no_amendements(app, lecture_an_url, user_david): assert resp.status_code == 200 assert "Les amendements ne sont pas encore disponibles." in resp.text + assert "Un problème ?" in resp.text + assert "Vos amendements n’apparaissent pas ?" in resp.text def test_get_amendements_order_default(app, lecture_an_url, amendements_an, user_david): @@ -30,9 +32,10 @@ def test_get_amendements_order_default(app, lecture_an_url, amendements_an, user "999", ] headers_rows_length = 3 - assert [" ".join(node.text().strip().split()) for node in resp.parser.css("tr")][ - headers_rows_length: - ] == ["Art. 1 666 Voir", "Art. 1 999 Voir"] + assert [ + " ".join(node.text().strip().split()) + for node in resp.parser.css("tr:not(.dropzone)") + ][headers_rows_length:] == ["Art. 1 666 Voir", "Art. 1 999 Voir"] def test_get_amendements_order_abandoned_last( @@ -51,9 +54,10 @@ def test_get_amendements_order_abandoned_last( assert resp.status_code == 200 headers_rows_length = 3 - assert [" ".join(node.text().strip().split()) for node in resp.parser.css("tr")][ - headers_rows_length: - ] == [ + assert [ + " ".join(node.text().strip().split()) + for node in resp.parser.css("tr:not(.dropzone)") + ][headers_rows_length:] == [ "Art. 1 999 Voir", ( "Les amendements en-deçà de cette ligne ne sont pas (encore) présents " @@ -78,9 +82,10 @@ def test_get_amendements_order_with_missing_position( assert resp.status_code == 200 headers_rows_length = 3 - assert [" ".join(node.text().strip().split()) for node in resp.parser.css("tr")][ - headers_rows_length: - ] == [ + assert [ + " ".join(node.text().strip().split()) + for node in resp.parser.css("tr:not(.dropzone)") + ][headers_rows_length:] == [ "Art. 1 999 Voir", ( "Les amendements en-deçà de cette ligne ne sont pas (encore) présents " @@ -105,9 +110,10 @@ def test_get_amendements_order_with_abandoned_next_do_not_display_limit_derouleu assert resp.status_code == 200 headers_rows_length = 3 - assert [" ".join(node.text().strip().split()) for node in resp.parser.css("tr")][ - headers_rows_length: - ] == ["Art. 1 999 Voir", "Art. 1 666 Irr. Voir"] + assert [ + " ".join(node.text().strip().split()) + for node in resp.parser.css("tr:not(.dropzone)") + ][headers_rows_length:] == ["Art. 1 999 Voir", "Art. 1 666 Irr. Voir"] def test_get_amendements_not_found_bad_format(app, user_david): @@ -158,7 +164,7 @@ def test_get_amendements_columns_too_many_amendements( with transaction.manager: for i in range(nb_amendements): - Amendement.create(lecture=lecture_an, article=article1_an, num=i + 1) + Amendement.create(lecture=lecture_an, article=article1_an, num=str(i + 1)) resp = app.get(f"{lecture_an_url}/amendements/", user=user_david) diff --git a/repondeur/tests/views/test_manual_refresh.py b/repondeur/tests/views/test_manual_refresh.py index 1cd4b96a5..68773fa0e 100644 --- a/repondeur/tests/views/test_manual_refresh.py +++ b/repondeur/tests/views/test_manual_refresh.py @@ -61,7 +61,9 @@ def test_post_form(app, lecture_an, lecture_an_url, article1_an, user_david): with transaction.manager: DBSession.add(lecture_an) lecture_an.texte.date_depot = datetime.utcnow().date() - timedelta(days=5) - Amendement.create(lecture=lecture_an, article=article1_an, num=135, position=1) + Amendement.create( + lecture=lecture_an, article=article1_an, num="135", position=1 + ) assert lecture_an.events == [] # No progress status by default. diff --git a/repondeur/tests/views/test_upload_csv.py b/repondeur/tests/views/test_upload_csv.py index 041a080d4..5159d4dfe 100644 --- a/repondeur/tests/views/test_upload_csv.py +++ b/repondeur/tests/views/test_upload_csv.py @@ -70,13 +70,13 @@ def test_upload_updates_user_content(self, app, filename, user_david): ReponseAmendementModifiee, ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None assert amendement.events == [] - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None @@ -84,7 +84,7 @@ def test_upload_updates_user_content(self, app, filename, user_david): self._upload_csv(app, filename, user=user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.user_content.avis == "Défavorable" assert "ipsum" in amendement.user_content.objet assert "amet" not in amendement.user_content.objet @@ -97,7 +97,7 @@ def test_upload_updates_user_content(self, app, filename, user_david): assert ObjetAmendementModifie in events assert ReponseAmendementModifiee in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.user_content.objet.startswith("Lorem") events = {type(event): event for event in amendement.events} assert ObjetAmendementModifie in events @@ -106,18 +106,18 @@ def test_upload_updates_user_content(self, app, filename, user_david): def test_upload_does_not_update_position(self, app, filename, user_david): from zam_repondeur.models import DBSession, Amendement - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.position == 1 - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.position == 2 self._upload_csv(app, filename, user=user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.position == 1 - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.position == 2 @pytest.mark.parametrize("filename", TEST_FILES) @@ -137,12 +137,12 @@ def test_upload_with_comments(self, app, user_david): self._upload_csv(app, "reponses_with_comments.csv", user=user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.user_content.comments == "A comment" events = {type(event): event for event in amendement.events} assert CommentsAmendementModifie in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.user_content.comments is None events = {type(event): event for event in amendement.events} assert CommentsAmendementModifie not in events @@ -153,21 +153,21 @@ def test_upload_with_affectation_to_unknown_user_without_team( from zam_repondeur.models import DBSession, Amendement from zam_repondeur.models.events.amendement import AmendementTransfere - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table is None - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None self._upload_csv(app, "reponses_with_affectation.csv", user=user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table.user.email == "melodie@exemple.gouv.fr" assert amendement.location.user_table.user.name == "Mélodie Dahi" events = {type(event): event for event in amendement.events} assert AmendementTransfere in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None events = {type(event): event for event in amendement.events} assert AmendementTransfere not in events @@ -175,14 +175,14 @@ def test_upload_with_affectation_to_unknown_user_without_team( def test_upload_with_affectation_empty_name(self, app, user_david): from zam_repondeur.models import DBSession, Amendement - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table is None self._upload_csv( app, "reponses_with_affectation_empty_name.csv", user=user_david ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table.user.email == "melodie@exemple.gouv.fr" assert amendement.location.user_table.user.name == "melodie@exemple.gouv.fr" @@ -198,10 +198,10 @@ def test_upload_with_affectation_to_unknown_user_with_team( DBSession.add(team_zam) team_zam_users = team_zam.users - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table is None - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None user_melodie = ( @@ -228,12 +228,12 @@ def test_upload_with_affectation_to_unknown_user_with_team( assert user_melodie.teams == [team_zam] # Check the amendement is on the new user's table - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table.user is user_melodie events = {type(event): event for event in amendement.events} assert AmendementTransfere in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None events = {type(event): event for event in amendement.events} assert AmendementTransfere not in events @@ -247,13 +247,13 @@ def test_upload_with_affectation_to_known_user(self, app, user_david): self._upload_csv(app, "reponses_with_affectation.csv", user=user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table.user.email == "melodie@exemple.gouv.fr" assert amendement.location.user_table.user.name == "Mélodie Dahi" events = {type(event): event for event in amendement.events} assert AmendementTransfere in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None events = {type(event): event for event in amendement.events} assert AmendementTransfere not in events @@ -308,7 +308,7 @@ def test_post_form_from_export( reponse="Une réponse très appropriée", comments="Avec des commentaires", ) - for position, num in enumerate((333, 777), 1) + for position, num in enumerate(("333", "777"), 1) ] counter = export_csv(lecture_an, filename, request={}) @@ -333,7 +333,7 @@ def test_post_form_from_export( assert resp.status_code == 200 assert "2 réponse(s) chargée(s) avec succès" in resp.text - amendement = DBSession.query(Amendement).filter(Amendement.num == 333).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "333").first() assert amendement.user_content.avis == "Favorable" - amendement = DBSession.query(Amendement).filter(Amendement.num == 777).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "777").first() assert amendement.user_content.avis == "Favorable" diff --git a/repondeur/tests/views/test_upload_json.py b/repondeur/tests/views/test_upload_json.py index 0ea1c99e5..72d84baa1 100644 --- a/repondeur/tests/views/test_upload_json.py +++ b/repondeur/tests/views/test_upload_json.py @@ -82,19 +82,19 @@ def test_upload_updates_user_content(self, app, user_david): ReponseAmendementModifiee, ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.user_content.avis is None assert amendement.user_content.objet is None assert amendement.user_content.reponse is None self._upload_backup(app, "backup.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.user_content.avis == "Défavorable" assert "ipsum" in amendement.user_content.objet assert "amet" not in amendement.user_content.objet @@ -107,7 +107,7 @@ def test_upload_updates_user_content(self, app, user_david): assert ObjetAmendementModifie in events assert ReponseAmendementModifiee in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.user_content.objet.startswith("Lorem") events = {type(event): event for event in amendement.events} assert ObjetAmendementModifie in events @@ -115,18 +115,18 @@ def test_upload_updates_user_content(self, app, user_david): def test_upload_does_not_update_position(self, app, user_david): from zam_repondeur.models import DBSession, Amendement - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.position == 1 - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.position == 2 self._upload_backup(app, "backup.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.position == 1 - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.position == 2 def test_upload_backup_with_comments(self, app, user_david): @@ -135,12 +135,12 @@ def test_upload_backup_with_comments(self, app, user_david): self._upload_backup(app, "backup_with_comments.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.user_content.comments == "A comment" events = {type(event): event for event in amendement.events} assert CommentsAmendementModifie in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.user_content.comments is None events = {type(event): event for event in amendement.events} assert CommentsAmendementModifie not in events @@ -151,22 +151,22 @@ def test_upload_backup_with_affectation_to_unknown_user_without_team( from zam_repondeur.models import DBSession, Amendement from zam_repondeur.models.events.amendement import AmendementTransfere - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table is None - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None self._upload_backup(app, "backup_with_affectation_new.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table.user.email == "melodie@exemple.gouv.fr" assert amendement.location.user_table.user.name == "Mélodie" assert amendement.location.user_table.user.teams[0].pk == team_zam.pk events = {type(event): event for event in amendement.events} assert AmendementTransfere in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None events = {type(event): event for event in amendement.events} assert AmendementTransfere not in events @@ -177,10 +177,10 @@ def test_upload_backup_with_affectation_to_unknown_user_with_team( from zam_repondeur.models import DBSession, Amendement, User from zam_repondeur.models.events.amendement import AmendementTransfere - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table is None - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None user_melodie = ( @@ -209,12 +209,12 @@ def test_upload_backup_with_affectation_to_unknown_user_with_team( assert user_melodie.teams == [team_zam] # Check the amendement is on the new user's table - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table.user is user_melodie events = {type(event): event for event in amendement.events} assert AmendementTransfere in events - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None events = {type(event): event for event in amendement.events} assert AmendementTransfere not in events @@ -225,40 +225,40 @@ def test_upload_updates_affectation(self, app, lecture_an, user_david, user_rona with transaction.manager: DBSession.add_all([user_david, user_ronan]) amendement = ( - DBSession.query(Amendement).filter(Amendement.num == 666).first() + DBSession.query(Amendement).filter(Amendement.num == "666").first() ) amendement.location.user_table = user_david.table_for(lecture_an) assert amendement.location.user_table.user.email == "david@exemple.gouv.fr" assert amendement.location.user_table.user.name == "David" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None self._upload_backup(app, "backup_with_affectation_existing.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.user_table.user.email == "ronan@exemple.gouv.fr" assert ( amendement.location.user_table.user.name == "Ronan" ) # Should not override the name of an existing user. - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.user_table is None def test_upload_affectation_box_new(self, app, lecture_an, user_david): from zam_repondeur.models import DBSession, Amendement from zam_repondeur.models.events.amendement import AmendementTransfere - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table is None - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None self._upload_backup(app, "backup_with_affectation_box.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table.titre == "PréRIM" # A transfer event has been created @@ -278,7 +278,7 @@ def test_upload_affectation_box_new(self, app, lecture_an, user_david): "a transféré l’amendement à « PréRIM »." ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None def test_upload_affectation_box_existing_not_updated( @@ -290,20 +290,20 @@ def test_upload_affectation_box_existing_not_updated( with transaction.manager: DBSession.add_all([user_david]) amendement = ( - DBSession.query(Amendement).filter(Amendement.num == 666).first() + DBSession.query(Amendement).filter(Amendement.num == "666").first() ) shared_table = SharedTable.create(lecture=lecture_an, titre="PréRIM") shared_table.add_amendement(amendement) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table.titre == "PréRIM" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None self._upload_backup(app, "backup_with_affectation_box.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table.titre == "PréRIM" # No transfer event has been created @@ -313,7 +313,7 @@ def test_upload_affectation_box_existing_not_updated( if isinstance(event, AmendementTransfere) ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None def test_upload_affectation_box_existing_updated(self, app, lecture_an, user_david): @@ -323,20 +323,20 @@ def test_upload_affectation_box_existing_updated(self, app, lecture_an, user_dav with transaction.manager: DBSession.add_all([user_david]) amendement = ( - DBSession.query(Amendement).filter(Amendement.num == 666).first() + DBSession.query(Amendement).filter(Amendement.num == "666").first() ) shared_table = SharedTable.create(lecture=lecture_an, titre="Initial") shared_table.add_amendement(amendement) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table.titre == "Initial" - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None self._upload_backup(app, "backup_with_affectation_box.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table.titre == "PréRIM" # A transfer event has been created @@ -356,7 +356,7 @@ def test_upload_affectation_box_existing_updated(self, app, lecture_an, user_dav "a transféré l’amendement de « Initial » à « PréRIM »." ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None def test_upload_affectation_box_from_user( @@ -368,20 +368,20 @@ def test_upload_affectation_box_from_user( with transaction.manager: DBSession.add_all([user_david, user_ronan]) amendement = ( - DBSession.query(Amendement).filter(Amendement.num == 666).first() + DBSession.query(Amendement).filter(Amendement.num == "666").first() ) amendement.location.user_table = user_ronan.table_for(lecture_an) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table is None assert amendement.location.user_table is not None - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None self._upload_backup(app, "backup_with_affectation_box.json", user_david) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.location.shared_table.titre == "PréRIM" # A transfer event has been created @@ -402,7 +402,7 @@ def test_upload_affectation_box_from_user( "à « PréRIM »." ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 999).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "999").first() assert amendement.location.shared_table is None def test_upload_backup_with_articles(self, app, user_david): @@ -418,7 +418,7 @@ def test_upload_backup_with_articles(self, app, user_david): in resp.text ) - amendement = DBSession.query(Amendement).filter(Amendement.num == 666).first() + amendement = DBSession.query(Amendement).filter(Amendement.num == "666").first() assert amendement.article.user_content.title == "Titre" assert amendement.article.user_content.presentation == "Présentation" @@ -472,7 +472,7 @@ def test_post_form_from_export( reponse="Une réponse très appropriée", comments="Avec des commentaires", ) - for position, num in enumerate((333, 777), 1) + for position, num in enumerate(("333", "777"), 1) ] counter = export_json(lecture_an, filename, request={}) diff --git a/repondeur/tests/views/test_upload_liasse_xml.py b/repondeur/tests/views/test_upload_liasse_xml.py index 8f921f915..0803dac59 100644 --- a/repondeur/tests/views/test_upload_liasse_xml.py +++ b/repondeur/tests/views/test_upload_liasse_xml.py @@ -101,11 +101,11 @@ def test_upload_liasse_with_table( ) assert ( "\n\n\n
Durée minimale de services" - in lecture.amendements[1].corps + in lecture.find_amendement("28").corps ) assert ( "\n\n\n
Durée minimale de services" - in lecture.amendements[0].expose + in lecture.find_amendement("26").expose ) diff --git a/repondeur/tests/views/test_visionneuse.py b/repondeur/tests/views/test_visionneuse.py index 5297aaef9..0474f39ad 100644 --- a/repondeur/tests/views/test_visionneuse.py +++ b/repondeur/tests/views/test_visionneuse.py @@ -224,7 +224,7 @@ def test_reponses_many_grouping( Amendement.create( lecture=lecture_an, article=article1_an, - num=42, + num="42", position=3, auteur="M. DUPONT", groupe="RDSE", @@ -235,7 +235,7 @@ def test_reponses_many_grouping( Amendement.create( lecture=lecture_an, article=article1_an, - num=57, + num="57", position=4, auteur="M. DURAND", groupe="Les Républicains", @@ -246,7 +246,7 @@ def test_reponses_many_grouping( Amendement.create( lecture=lecture_an, article=article1_an, - num=72, + num="72", position=5, auteur="M. MARTIN", groupe="Les Républicains", @@ -257,7 +257,7 @@ def test_reponses_many_grouping( Amendement.create( lecture=lecture_an, article=article1_an, - num=83, + num="83", position=6, auteur="M. MARTIN", groupe="Les Républicains", diff --git a/repondeur/tests/visam/conftest.py b/repondeur/tests/visam/conftest.py new file mode 100644 index 000000000..bac723b98 --- /dev/null +++ b/repondeur/tests/visam/conftest.py @@ -0,0 +1,26 @@ +import pytest + +from testapp import TestApp +from visam.fixtures.seances import * # noqa: F401,F403 +from visam.fixtures.users import * # noqa: F401,F403 + + +@pytest.fixture(scope="session") # noqa: F811 +def wsgi_app(settings, mock_dossiers, mock_organes_acteurs, mock_scraping_senat): + from zam_repondeur.visam.app import make_app + + return make_app(None, **settings) + + +@pytest.fixture +def app( + wsgi_app, db, whitelist, data_repository, users_repository, +): + yield TestApp( + wsgi_app, + extra_environ={ + "HTTP_HOST": "visam.test", + "REMOTE_ADDR": "127.0.0.1", + "wsgi.url_scheme": "https", + }, + ) diff --git a/repondeur/tests/visam/fixtures/__init__.py b/repondeur/tests/visam/fixtures/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/repondeur/tests/visam/fixtures/seances.py b/repondeur/tests/visam/fixtures/seances.py new file mode 100644 index 000000000..b18537aec --- /dev/null +++ b/repondeur/tests/visam/fixtures/seances.py @@ -0,0 +1,237 @@ +from datetime import date + +import pytest +import transaction + + +@pytest.fixture +def seance_ccfp(db, team_zam): + from zam_repondeur.models import Chambre + from zam_repondeur.visam.models import Seance, Formation + + with transaction.manager: + seance = Seance.create( + chambre=Chambre.CCFP, + formation=Formation.ASSEMBLEE_PLENIERE, + date=date(2020, 4, 1), + ) + seance.team = team_zam + + return seance + + +@pytest.fixture +def dossier_seance_ccfp(db, team_zam): + from zam_repondeur.models import Dossier + + with transaction.manager: + dossier = Dossier.create( + an_id="dummy-titre-texte-ccfp", + titre="Titre du texte CCFP", + slug="titre-texte-ccfp", + ) + dossier.team = team_zam + + return dossier + + +@pytest.fixture +def dossier_seance_ccfp_2(db, team_zam): + from zam_repondeur.models import Dossier + + with transaction.manager: + dossier = Dossier.create( + an_id="dummy-titre-texte-ccfp-2", + titre="Titre du texte CCFP 2", + slug="titre-texte-ccfp-2", + ) + dossier.team = team_zam + + return dossier + + +@pytest.fixture +def texte_seance_ccfp(db): + from zam_repondeur.models import Chambre, Texte, TypeTexte + + with transaction.manager: + texte = Texte.create( + type_=TypeTexte.PROJET, + numero=1, + chambre=Chambre.CCFP, + date_depot=date(2020, 4, 1), + ) + + return texte + + +@pytest.fixture +def lecture_seance_ccfp(db, seance_ccfp, dossier_seance_ccfp, texte_seance_ccfp): + from zam_repondeur.models import DBSession, Lecture, Phase + + with transaction.manager: + lecture = Lecture.create( + phase=Phase.PREMIERE_LECTURE, + texte=texte_seance_ccfp, + titre="Première lecture – Assemblée plénière", + organe="Assemblée plénière", + dossier=dossier_seance_ccfp, + ) + DBSession.add(seance_ccfp) + seance_ccfp.lectures.append(lecture) + + return lecture + + +@pytest.fixture +def article1_texte_seance_ccfp(db, lecture_seance_ccfp): + from zam_repondeur.models import Article + + with transaction.manager: + article = Article.create(lecture=lecture_seance_ccfp, type="article", num="1") + + return article + + +@pytest.fixture +def amendement_222_lecture_seance_ccfp( + db, lecture_seance_ccfp, article1_texte_seance_ccfp +): + from zam_repondeur.models import Amendement, DBSession + + with transaction.manager: + amendement = Amendement.create( + lecture=lecture_seance_ccfp, + article=article1_texte_seance_ccfp, + num="v222", + position=1, + ) + + DBSession.add(amendement) + return amendement + + +@pytest.fixture +def amendement_444_lecture_seance_ccfp( + db, lecture_seance_ccfp, article1_texte_seance_ccfp +): + from zam_repondeur.models import Amendement, DBSession + + with transaction.manager: + amendement = Amendement.create( + lecture=lecture_seance_ccfp, + article=article1_texte_seance_ccfp, + num="v444", + position=2, + ) + + DBSession.add(amendement) + return amendement + + +@pytest.fixture +def lecture_seance_ccfp_2(db, seance_ccfp, dossier_seance_ccfp_2, texte_seance_ccfp): + from zam_repondeur.models import DBSession, Lecture, Phase + + with transaction.manager: + lecture = Lecture.create( + phase=Phase.PREMIERE_LECTURE, + texte=texte_seance_ccfp, + titre="Première lecture – Assemblée plénière", + organe="Assemblée plénière", + dossier=dossier_seance_ccfp_2, + ) + DBSession.add(seance_ccfp) + seance_ccfp.lectures.append(lecture) + + return lecture + + +@pytest.fixture +def articles_seance_ccfp(db, lecture_seance_ccfp): + from zam_repondeur.models import Article + + articles = [] + with transaction.manager: + content = { + "1": ["Contenu article 1"], + "2": ["Contenu article 2 alinéa 1", "Contenu article 2 alinéa 2"], + } + for num, alineas in content.items(): + article = Article.create( + type="article", + num=num, + mult="", + pos="", + lecture=lecture_seance_ccfp, + content={ + str(i).zfill(3): alinea for i, alinea in enumerate(alineas, start=1) + }, + ) + articles.append(article) + + return articles + + +@pytest.fixture +def seance_csfpe(db, team_zam): + from zam_repondeur.models import Chambre + from zam_repondeur.visam.models import Seance, Formation + + with transaction.manager: + seance = Seance.create( + chambre=Chambre.CSFPE, + formation=Formation.ASSEMBLEE_PLENIERE, + date=date(2020, 5, 15), + ) + seance.team = team_zam + + return seance + + +@pytest.fixture +def dossier_seance_csfpe(db, team_zam): + from zam_repondeur.models import Dossier + + with transaction.manager: + dossier = Dossier.create( + an_id="dummy-titre-texte-csfpe", + titre="Titre du texte CSFPE", + slug="titre-texte-csfpe", + ) + dossier.team = team_zam + + return dossier + + +@pytest.fixture +def texte_seance_csfpe(db): + from zam_repondeur.models import Chambre, Texte, TypeTexte + + with transaction.manager: + texte = Texte.create( + type_=TypeTexte.PROJET, + numero=2, + chambre=Chambre.CSFPE, + date_depot=date(2020, 4, 1), + ) + + return texte + + +@pytest.fixture +def lecture_seance_csfpe(db, seance_csfpe, dossier_seance_csfpe, texte_seance_csfpe): + from zam_repondeur.models import DBSession, Lecture, Phase + + with transaction.manager: + lecture = Lecture.create( + phase=Phase.PREMIERE_LECTURE, + texte=texte_seance_csfpe, + titre="Première lecture – Assemblée plénière", + organe="Assemblée plénière", + dossier=dossier_seance_csfpe, + ) + DBSession.add(seance_csfpe) + seance_csfpe.lectures.append(lecture) + + return lecture diff --git a/repondeur/tests/visam/fixtures/users.py b/repondeur/tests/visam/fixtures/users.py new file mode 100644 index 000000000..c73562b64 --- /dev/null +++ b/repondeur/tests/visam/fixtures/users.py @@ -0,0 +1,94 @@ +from datetime import datetime + +import pytest +import transaction + + +@pytest.fixture +def user_admin(db): + from zam_repondeur.models import User + + with transaction.manager: + return User.create( + name="Admin user", email="user@admin.gouv.fr", admin_at=datetime.utcnow() + ) + + +@pytest.fixture +def org_gouvernement(db): + from zam_repondeur.models import DBSession + from zam_repondeur.visam.models import Organisation + + with transaction.manager: + organisation = Organisation(name="Gouvernement") + DBSession.add(organisation) + + return organisation + + +@pytest.fixture +def org_cgt(db): + from zam_repondeur.models import DBSession + from zam_repondeur.visam.models import Organisation + + with transaction.manager: + organisation = Organisation(name="CGT") + DBSession.add(organisation) + + return organisation + + +@pytest.fixture +def user_ccfp(db, user_david, org_cgt): + from zam_repondeur.models import DBSession, Chambre + from zam_repondeur.visam.models import UserMembership + + with transaction.manager: + user_membership = UserMembership( + user=user_david, chambre=Chambre.CCFP, organisation=org_cgt + ) + DBSession.add(user_membership) + + return user_david + + +@pytest.fixture +def user_ccfp_gouvernement(db, user_david, org_gouvernement): + from zam_repondeur.models import DBSession, Chambre + from zam_repondeur.visam.models import UserMembership + + with transaction.manager: + user_membership = UserMembership( + user=user_david, chambre=Chambre.CCFP, organisation=org_gouvernement + ) + DBSession.add(user_membership) + + return user_david + + +@pytest.fixture +def user_csfpe(db, user_david, org_cgt): + from zam_repondeur.models import DBSession, Chambre + from zam_repondeur.visam.models import UserMembership + + with transaction.manager: + user_membership = UserMembership( + user=user_david, chambre=Chambre.CSFPE, organisation=org_cgt + ) + DBSession.add(user_membership) + + return user_david + + +@pytest.fixture +def user_csfpe_gouvernement(db, user_david, org_gouvernement): + from zam_repondeur.models import DBSession, Chambre + from zam_repondeur.visam.models import UserMembership + + with transaction.manager: + user_membership = UserMembership( + user=user_david, chambre=Chambre.CSFPE, organisation=org_gouvernement + ) + DBSession.add(user_membership) + + return user_david diff --git a/repondeur/tests/visam/integration_visam/__init__.py b/repondeur/tests/visam/integration_visam/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/repondeur/tests/visam/integration_visam/conftest.py b/repondeur/tests/visam/integration_visam/conftest.py new file mode 100644 index 000000000..4b2092da1 --- /dev/null +++ b/repondeur/tests/visam/integration_visam/conftest.py @@ -0,0 +1,115 @@ +from contextlib import contextmanager +from functools import partial + +import pytest +from selenium import webdriver +from selenium.common.exceptions import WebDriverException +from webtest.http import StopableWSGIServer + +from .helpers import login, logout + + +@pytest.fixture(params=["firefox", "chrome"]) +def driver(request, wsgi_server): + factory = driver_factory(request.param) + with factory() as _driver: + + # Add helper methods to the driver + _driver.login = partial(login, _driver, wsgi_server.application_url) + _driver.logout = partial(logout, _driver, wsgi_server.application_url) + + try: + yield _driver + finally: + _driver.quit() + + +def driver_factory(name): + if name == "firefox": + return firefox_driver + elif name == "chrome": + return chrome_driver + + +@contextmanager +def firefox_driver(): + try: + yield HeadlessFirefox() + except WebDriverException as e: + if str(e).startswith("Message: 'geckodriver' executable needs to be in PATH."): + pytest.skip("You need geckodriver to run browser tests in Firefox") + else: + raise + + +@contextmanager +def chrome_driver(): + try: + yield HeadlessChrome() + except WebDriverException as e: + if str(e).startswith("Message: 'chromedriver' executable needs to be in PATH."): + pytest.skip("You need chromedriver to run browser tests in Chrome") + else: + raise + + +@pytest.fixture +def wsgi_server( + settings, db, mock_dossiers, mock_organes_acteurs, amendements_repository +): + from zam_repondeur.visam.app import make_app + + settings = {**settings, "zam.auth_cookie_secure": False} + wsgi_app = make_app(None, **settings) + server = StopableWSGIServer.create(wsgi_app) + server.settings = settings + yield server + server.shutdown() + + +class HeadlessFirefox(webdriver.Firefox): + def __init__(self): + super().__init__(options=self.options()) + + @staticmethod + def options(): + firefox_options = webdriver.firefox.options.Options() + firefox_options.add_argument("-headless") + firefox_options.set_preference("dom.disable_beforeunload", False) + return firefox_options + + def get(self, url): + """ + Loads a web page in the current browser session. + + We override the default `get` to force a page refresh if the target URL is + the currently open one, as Selenium / geckodriver / Firefox (not sure who is + guilty here) will just skip the request in that case, even if the page has + HTTP headers asking for it not to be cached. + """ + if url == self.current_url: + self.refresh() + else: + super().get(url) + + +class HeadlessChrome(webdriver.Chrome): + def __init__(self): + super().__init__(options=self.options()) + + @staticmethod + def options(): + chrome_options = webdriver.chrome.options.Options() + chrome_options.add_argument("--headless") + chrome_options.add_argument("--no-sandbox") + return chrome_options + + +@pytest.fixture +def seance_ccfp_url(wsgi_server, seance_ccfp): + return f"{wsgi_server.application_url}seances/{seance_ccfp.slug}/" + + +@pytest.fixture +def lecture_seance_ccfp_url(seance_ccfp_url, lecture_seance_ccfp): + return f"{seance_ccfp_url}textes/{lecture_seance_ccfp.dossier.slug}" diff --git a/repondeur/tests/visam/integration_visam/helpers.py b/repondeur/tests/visam/integration_visam/helpers.py new file mode 100644 index 000000000..b836ba0db --- /dev/null +++ b/repondeur/tests/visam/integration_visam/helpers.py @@ -0,0 +1,31 @@ +from selenium.webdriver.support.ui import WebDriverWait + +from zam_repondeur.auth import generate_auth_token +from zam_repondeur.services.users import repository + + +def login(driver, base_url, email): + wait = WebDriverWait(driver, 1) + + # Click on authentication link + token = generate_auth_token() + repository.set_auth_token(email, token) + driver.get(f"{base_url}authentification?token={token}") + + # Submit name on first login + welcome_url = f"{base_url}bienvenue" + if not driver.current_url.startswith(welcome_url): + return # name already known + assert ( + driver.find_element_by_css_selector("h1").text + == "C’est votre première connexion…" + ) + driver.find_element_by_css_selector("input[type='submit']").click() + wait.until(lambda driver: not driver.current_url.startswith(welcome_url)) + + +def logout(driver, base_url): + logout_url = f"{base_url}deconnexion" + driver.get(logout_url) + wait = WebDriverWait(driver, 1) + wait.until(lambda driver: not driver.current_url.startswith(logout_url)) diff --git a/repondeur/tests/visam/integration_visam/test_amendement_creation.py b/repondeur/tests/visam/integration_visam/test_amendement_creation.py new file mode 100644 index 000000000..fcd94fa5f --- /dev/null +++ b/repondeur/tests/visam/integration_visam/test_amendement_creation.py @@ -0,0 +1,170 @@ +import time +from textwrap import dedent + +import transaction +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import Select, WebDriverWait + + +def test_amendement_creation_select_article( + driver, user_ccfp, lecture_seance_ccfp_url, articles_seance_ccfp +): + driver.login(user_ccfp.email) + driver.get(f"{lecture_seance_ccfp_url}/amendements/saisie") + subdiv = Select(driver.find_element_by_css_selector('select[name="subdiv"]')) + subdiv.select_by_visible_text("Art. 2") + time.sleep(1) # Wait for the option to be selected. + + assert ( + driver.find_element_by_css_selector('[data-target="preview.contents"]').text + == dedent( + """ + 001 + Contenu article 2 alinéa 1 + 002 + Contenu article 2 alinéa 2 + """ + ).strip() + ) + + +class TestAddAmendement: + def test_member_adds_amendement_for_their_organization( + app, + driver, + lecture_seance_ccfp_url, + articles_seance_ccfp, + user_ccfp, + org_gouvernement, + org_cgt, + ): + from zam_repondeur.models import DBSession, Amendement + + driver.login(user_ccfp.email) + driver.get(f"{lecture_seance_ccfp_url}/amendements/saisie") + subdiv = Select(driver.find_element_by_css_selector('select[name="subdiv"]')) + subdiv.select_by_visible_text("Art. 2") + + driver.switch_to.frame("corps_ifr") + WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-id="corps"]',)) + ).send_keys("Corps") + + driver.switch_to.default_content() # Required to switch to a new iframe. + + driver.switch_to.frame("expose_ifr") + WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-id="expose"]',)) + ).send_keys("Exposé") + + driver.switch_to.default_content() + save_button = driver.find_element_by_css_selector( + '.save-buttons input[name="save"]' + ) + save_button.click() + + with transaction.manager: + amendement = DBSession.query(Amendement).first() + assert amendement.num == "CGT 1" + assert amendement.groupe == "CGT" + assert amendement.article.num == "2" + assert amendement.corps == "

Corps

" + assert amendement.expose == "

Exposé

" + + def test_gouvernement_adds_amendement_for_an_organization( + app, + driver, + lecture_seance_ccfp_url, + articles_seance_ccfp, + user_ccfp_gouvernement, + org_gouvernement, + org_cgt, + ): + from zam_repondeur.models import DBSession, Amendement + + driver.login(user_ccfp_gouvernement.email) + driver.get(f"{lecture_seance_ccfp_url}/amendements/saisie") + subdiv = Select(driver.find_element_by_css_selector('select[name="subdiv"]')) + subdiv.select_by_visible_text("Art. 2") + + organisation = Select( + driver.find_element_by_css_selector('select[name="organisation"]') + ) + organisation.select_by_visible_text("CGT") + + driver.switch_to.frame("corps_ifr") + WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-id="corps"]',)) + ).send_keys("Corps") + + driver.switch_to.default_content() # Required to switch to a new iframe. + + driver.switch_to.frame("expose_ifr") + WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-id="expose"]',)) + ).send_keys("Exposé") + + driver.switch_to.default_content() + save_button = driver.find_element_by_css_selector( + '.save-buttons input[name="save"]' + ) + save_button.click() + + with transaction.manager: + amendement = DBSession.query(Amendement).first() + assert amendement.num == "CGT 1" + assert amendement.groupe == "CGT" + assert amendement.article.num == "2" + assert amendement.corps == "

Corps

" + assert amendement.expose == "

Exposé

" + + def test_admin_adds_amendement_for_an_organization( + app, + driver, + lecture_seance_ccfp_url, + articles_seance_ccfp, + user_admin, + org_gouvernement, + org_cgt, + ): + from zam_repondeur.models import DBSession, Amendement, Chambre + + DBSession.add(user_admin) + assert user_admin.membership_of(Chambre.CCFP) is None + + driver.login(user_admin.email) + driver.get(f"{lecture_seance_ccfp_url}/amendements/saisie") + subdiv = Select(driver.find_element_by_css_selector('select[name="subdiv"]')) + subdiv.select_by_visible_text("Art. 2") + + organisation = Select( + driver.find_element_by_css_selector('select[name="organisation"]') + ) + organisation.select_by_visible_text("CGT") + + driver.switch_to.frame("corps_ifr") + WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-id="corps"]',)) + ).send_keys("Corps") + + driver.switch_to.default_content() # Required to switch to a new iframe. + + driver.switch_to.frame("expose_ifr") + WebDriverWait(driver, 20).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-id="expose"]',)) + ).send_keys("Exposé") + + driver.switch_to.default_content() + save_button = driver.find_element_by_css_selector( + '.save-buttons input[name="save"]' + ) + save_button.click() + + with transaction.manager: + amendement = DBSession.query(Amendement).first() + assert amendement.num == "CGT 1" + assert amendement.groupe == "CGT" + assert amendement.article.num == "2" + assert amendement.corps == "

Corps

" + assert amendement.expose == "

Exposé

" diff --git a/repondeur/tests/visam/projet_de_decret.html b/repondeur/tests/visam/projet_de_decret.html new file mode 100644 index 000000000..01ed5b66c --- /dev/null +++ b/repondeur/tests/visam/projet_de_decret.html @@ -0,0 +1,145 @@ +

Le 17 mars 2020

+

 

+

 

+

JORF n°0212 du 12 septembre 2019

+

 

+

Texte n°8

+

 

+

 

+

Décret n° 2019-947 du 10 septembre 2019 modifiant le décret n° 2008-836 du 22 août 2008 fixant l’échelonnement indiciaire des corps et des emplois communs aux administrations de l’Etat et de ses établissements publics ou afférent à plusieurs corps de fonctionnaires de l’Etat et de ses établissements publics

+

 

+

NOR: TREK1911660D

+

 

+

 

+

ELI:https://www.legifrance.gouv.fr/eli/decret/2019/9/10/TREK1911660D/jo/texte

+

Alias: https://www.legifrance.gouv.fr/eli/decret/2019/9/10/2019-947/jo/texte

+

 

+

 

+

 

+

 

+

Publics concernés : fonctionnaires appartenant au corps des architectes et urbanistes de l’Etat.

+

 

+

Objet : modification de la grille indiciaire du corps des architectes et urbanistes de l’Etat.

+

 

+

Entrée en vigueur : le texte entre en vigueur le lendemain de sa publication.

+

 

+

Notice : le décret modifie la grille indiciaire du grade des architectes et urbanistes de l’Etat en chef afin de tenir compte de la transformation de l’échelon spécial contingenté en un 8e échelon linéaire.

+

 

+

Références : le décret et le texte qu’il modifie, dans sa rédaction issue de cette modification, peuvent être consultés sur le site Légifrance (https://www.legifrance.gouv.fr).  

+

 

+

 

+

Le Premier ministre,

+

 

+

Sur le rapport de la ministre de la transition écologique et solidaire,

+

 

+

Vu la loi n° 83-634 du 13 juillet 1983 modifiée portant droits et obligations des fonctionnaires, ensemble la loi n° 84-16 du 11 janvier 1984 modifiée portant dispositions statutaires relatives à la fonction publique de l’Etat ;

+

 

+

Vu le décret n° 2004-474 du 2 juin 2004 modifié portant statut du corps des architectes et urbanistes de l’Etat ;

+

 

+

Vu le décret n° 2008-836 du 22 août 2008 fixant l’échelonnement indiciaire des corps et des emplois communs aux administrations de l’Etat et de ses établissements publics ou afférent à plusieurs corps de fonctionnaires de l’Etat et de ses établissements publics ;

+

 

+

Vu l’avis du Conseil supérieur de la fonction publique de l’Etat en date du 10 avril 2019,

+

 

+

Décrète : 

+

 

+

 

+

Article 1

+

 

+

 

+

A l’article 2 du décret du 22 août 2008 susvisé, les lignes :  

+ + + + + + + + + + + + + + + + + + +
+

 

+

Architectes et urbanistes de l’Etat en chef

+
+

 

+

ES

+
+

 

+

HEB bis

+
+

 

+

HEB bis

+
+

 

+
+

 

+

 

+

sont remplacées par les lignes :  

+ + + + + + + + + + + + + + + + + + +
+

 

+

Architectes et urbanistes de l’Etat en chef

+
+

 

+

8

+
+

 

+

HEB bis

+
+

 

+

HEB bis

+
+

 

+
+

 

+

 

+

Article 2

+

 

+

 

+

La ministre de la transition écologique et solidaire, le ministre de l’action et des comptes publics, le ministre de la culture et le secrétaire d’Etat auprès du ministre de l’action et des comptes publics sont chargés, chacun en ce qui le concerne, de l’exécution du présent décret, qui sera publié au Journal officiel de la République française. 

+

 

+

 

+

Fait le 10 septembre 2019. 

+

 

+

Edouard Philippe 

+

Par le Premier ministre : 

+

 

+

La ministre de la transition écologique et solidaire, 

+

Elisabeth Borne 

+

 

+

Le ministre de l’action et des comptes publics, 

+

Gérald Darmanin 

+

 

+

Le ministre de la culture, 

+

Franck Riester 

+

 

+

Le secrétaire d’Etat auprès du ministre de l’action et des comptes publics, 

+

Olivier Dussopt 

+

 

+

 

+

 

\ No newline at end of file diff --git a/repondeur/tests/visam/test_amendement_saisie.py b/repondeur/tests/visam/test_amendement_saisie.py new file mode 100644 index 000000000..fa629fbbc --- /dev/null +++ b/repondeur/tests/visam/test_amendement_saisie.py @@ -0,0 +1,173 @@ +from webtest.forms import Select + + +class TestButtonToAddAmendementOnIndex: + def test_ccfp(self, app, user_ccfp, seance_ccfp, lecture_seance_ccfp): + lecture = lecture_seance_ccfp + dossier = lecture.dossier + resp = app.get( + f"/seances/{seance_ccfp.slug}/textes/{dossier.slug}/amendements/", + user=user_ccfp, + ) + assert resp.status_code == 200 + assert "Aucun amendement saisi pour l’instant…" in resp + assert "Saisir un nouvel amendement" in resp + + def test_csfpe(self, app, user_csfpe, seance_csfpe, lecture_seance_csfpe): + lecture = lecture_seance_csfpe + dossier = lecture.dossier + resp = app.get( + f"/seances/{seance_csfpe.slug}/textes/{dossier.slug}/amendements/", + user=user_csfpe, + ) + assert resp.status_code == 200 + assert "Aucun amendement saisi pour l’instant…" in resp + assert "Saisir un nouvel amendement" in resp + + +class TestAmendementSaisieForm: + def test_get_form( + self, app, seance_ccfp, lecture_seance_ccfp, amendements_an, user_ccfp + ): + resp = app.get( + ( + f"/seances/{seance_ccfp.slug}" + f"/textes/{lecture_seance_ccfp.dossier.slug}/amendements/saisie" + ), + user=user_ccfp, + ) + + assert resp.status_code == 200 + assert resp.content_type == "text/html" + + form = resp.forms["saisie-amendement"] + assert form.method == "POST" + assert list(form.fields.keys()) == [ + "subdiv", + "corps", + "expose", + "save", + ] + + def test_get_form_gouvernement( + self, + app, + seance_ccfp, + lecture_seance_ccfp, + amendements_an, + user_ccfp_gouvernement, + org_cgt, + ): + resp = app.get( + ( + f"/seances/{seance_ccfp.slug}" + f"/textes/{lecture_seance_ccfp.dossier.slug}/amendements/saisie" + ), + user=user_ccfp_gouvernement, + ) + + assert resp.status_code == 200 + assert resp.content_type == "text/html" + + form = resp.forms["saisie-amendement"] + assert form.method == "POST" + assert list(form.fields.keys()) == [ + "subdiv", + "organisation", + "corps", + "expose", + "save", + ] + assert isinstance(form.fields["organisation"][0], Select) + assert form.fields["organisation"][0].options == [ + ("Gouvernement", True, "Gouvernement"), + ("CGT", False, "CGT"), + ] + + def test_post_form( + self, + app, + seance_ccfp, + lecture_seance_ccfp, + article1_texte_seance_ccfp, + user_ccfp, + ): + from zam_repondeur.models import Amendement, DBSession + + assert len(DBSession.query(Amendement).all()) == 0 + + resp = app.get( + ( + f"/seances/{seance_ccfp.slug}" + f"/textes/{lecture_seance_ccfp.dossier.slug}/amendements/saisie" + ), + user=user_ccfp, + ) + form = resp.forms["saisie-amendement"] + form["subdiv"] = "article.1.." + form[ + "corps" + ] = 'Un corps de rêve' + form["expose"] = "Avec un
exposé
" + resp = form.submit("save") + + assert resp.status_code == 302 + assert resp.location == ( + "https://visam.test/seances/ccfp-2020-04-01/" + "textes/titre-texte-ccfp/amendements/#amdt-CGT-1" + ) + + amendement = DBSession.query(Amendement).first() + assert amendement.num == "CGT 1" + assert amendement.groupe == "CGT" + assert amendement.article.pk == article1_texte_seance_ccfp.pk + assert amendement.corps == "Un corps de rêve" + assert ( + amendement.expose + == "Avec un
exposé
" + ) + + def test_post_form_gouvernement( + self, + app, + seance_ccfp, + lecture_seance_ccfp, + article1_texte_seance_ccfp, + user_ccfp_gouvernement, + org_cgt, + ): + from zam_repondeur.models import Amendement, DBSession + + assert len(DBSession.query(Amendement).all()) == 0 + + resp = app.get( + ( + f"/seances/{seance_ccfp.slug}" + f"/textes/{lecture_seance_ccfp.dossier.slug}/amendements/saisie" + ), + user=user_ccfp_gouvernement, + ) + form = resp.forms["saisie-amendement"] + form["subdiv"] = "article.1.." + form["organisation"] = "CGT" + form[ + "corps" + ] = 'Un corps de rêve' + form["expose"] = "Avec un
exposé
" + resp = form.submit("save") + + assert resp.status_code == 302 + assert resp.location == ( + "https://visam.test/seances/ccfp-2020-04-01/" + "textes/titre-texte-ccfp/amendements/#amdt-CGT-1" + ) + + amendement = DBSession.query(Amendement).first() + assert amendement.num == "CGT 1" + assert amendement.groupe == "CGT" + assert amendement.article.pk == article1_texte_seance_ccfp.pk + assert amendement.corps == "Un corps de rêve" + assert ( + amendement.expose + == "Avec un
exposé
" + ) diff --git a/repondeur/tests/visam/test_amendements_order.py b/repondeur/tests/visam/test_amendements_order.py new file mode 100644 index 000000000..401b5a26f --- /dev/null +++ b/repondeur/tests/visam/test_amendements_order.py @@ -0,0 +1,61 @@ +import pytest + + +@pytest.mark.usefixtures("amendement_222_lecture_seance_ccfp") +def test_lecture_reorder_amendements_unique_amendement( + app, seance_ccfp, lecture_seance_ccfp, user_ccfp +): + lecture = lecture_seance_ccfp + dossier = lecture.dossier + resp = app.get( + f"/seances/{seance_ccfp.slug}/textes/{dossier.slug}/amendements/", + user=user_ccfp, + ) + assert resp.status_code == 200 + assert ( + ' + {% if total_count_amendements > 1 %} + + {% endif %} {% endblock %} diff --git a/repondeur/zam_repondeur/templates/visionneuse/derouleur.html b/repondeur/zam_repondeur/templates/visionneuse/derouleur.html new file mode 100644 index 000000000..4815679e2 --- /dev/null +++ b/repondeur/zam_repondeur/templates/visionneuse/derouleur.html @@ -0,0 +1,135 @@ +{% extends "visionneuse/_base.html" %} +{% import "visionneuse/macros.html" as macros %} + +{% block extra_styles %} + +{% endblock %} + +{% block title %} +

{{ lecture.dossier.titre }}

+{% endblock %} + +{% block main %} + {% for article in articles %} + {% if article.slug and not article.is_erreur %} +
+
+
+
+
+

{{ article.format(short=False) }}

+
+ {% if article.content or article.user_content.presentation %} + Texte + {% endif %} +
+
+
+
+ +
+ +
+

{{ article.user_content.title or "" }}

+ {% if article.user_content.presentation %} +

Présentation de l’article

+ {{ article.user_content.presentation | safe }} + {% endif %} + {% if article.content %} +

Texte de l’article

+ {% for number, content in article.content.items() %} +
+
{{ number }}
+
+

{{ content.strip('"') | safe }}

+
+
+ {% endfor %} + {% endif %} +

+ Replier +

+
+
+ + {% for reponse, amendements in article.grouped_displayable_amendements() %} + {% set amendement = amendements[0] %} + {% set is_gouvernemental = amendement.gouvernemental %} + {% set parent = amendement.parent %} +
+ {% set multiple = amendements|length > 1 %} +
+
+

+ {{ macros.multiple_amendements_title(amendements) }} +

+ {% if amendement.user_content.objet and (amendement.user_content.objet|length < 200) %} +
+ {{ amendement.user_content.objet | paragriphy }} +
+ {% endif %} +
+
+
+
+ {% for amendement in amendements %} + {% if multiple %} +

+ Amendement {{ amendement.num }} + — {{ amendement.auteur }} + {% if amendement.groupe -%} + ({{ amendement.groupe }} + ) + {%- endif %} +

+ {% endif %} +
+

Exposé

+ {{ amendement.expose|safe }} +

Corps de l’amendement

+ {{ amendement.corps|safe }} +
+ {% endfor %} +
+
+
+ {% endfor %} + {% endif %} + {% endfor %} +{% endblock %} + + diff --git a/repondeur/zam_repondeur/templates/visionneuse/macros.html b/repondeur/zam_repondeur/templates/visionneuse/macros.html new file mode 100644 index 000000000..ec0460ee4 --- /dev/null +++ b/repondeur/zam_repondeur/templates/visionneuse/macros.html @@ -0,0 +1,19 @@ +{% macro multiple_amendements_title(amendements) %} + {% set amendement = amendements[0] %} + {% set parent = amendement.parent %} + {% set length = amendements|length %} + Amendement{% if length > 1 %}s{% endif %} {% if length > 5 %} + {{ amendements[0] }}, + {{ amendements[1] }}, + …, + {{ amendements[-3] }}, + {{ amendements[-2] }} et + {{ amendements[-1] }} + ({{ length|number }} au total) + {% else %} + {% for amendement in amendements %} + {{ amendement }}{% if loop.revindex == 2 %} et {% else %}{% if not loop.last %}, {% endif %}{% endif %} + {% endfor %} + {% endif %} + {% if parent %} (sous-amendement au {{ parent }}){% endif %} +{% endmacro %} diff --git a/repondeur/zam_repondeur/templates/visionneuse/reponses.html b/repondeur/zam_repondeur/templates/visionneuse/reponses.html index a2ceb333e..275001874 100644 --- a/repondeur/zam_repondeur/templates/visionneuse/reponses.html +++ b/repondeur/zam_repondeur/templates/visionneuse/reponses.html @@ -1,4 +1,5 @@ {% extends "visionneuse/_base.html" %} +{% import "visionneuse/macros.html" as macros %} {% block title %}
@@ -84,11 +85,11 @@

Texte de l’article

> + data-toggle-target="amendement-detail-{{ amendement.slug }}"> Texte

- {{ multiple_amendements_title(amendements) }} + {{ macros.multiple_amendements_title(amendements) }}

{% if amendement.user_content.objet and (amendement.user_content.objet|length < 200) %}
@@ -117,7 +118,7 @@

@@ -126,7 +127,7 @@

+ data-toggle-target="reponse-detail-{{ amendement.slug }}"> Réponse

Avis : {{ amendement.user_content.avis }}

@@ -134,7 +135,7 @@

{% endif %}
-
+
{% if amendement.user_content.has_objet %}

Objet

@@ -159,7 +160,7 @@

Réponse

-
{% for amendement in amendements %} {% if multiple %} @@ -206,24 +207,4 @@

Corps de l’amendement

{% endif %} {% endblock %} -{% macro multiple_amendements_title(amendements) %} - {% set amendement = amendements[0] %} - {% set parent = amendement.parent %} - {% set length = amendements|length %} - Amendement{% if length > 1 %}s{% endif %} {% if length > 5 %} - {{ amendements[0] }}, - {{ amendements[1] }}, - …, - {{ amendements[-3] }}, - {{ amendements[-2] }} et - {{ amendements[-1] }} - ({{ length|number }} au total) - {% else %} - {% for amendement in amendements %} - {{ amendement }}{% if loop.revindex == 2 %} et {% else %}{% if not loop.last %}, {% endif %}{% endif %} - {% endfor %} - {% endif %} - {% if parent %} (sous-amendement au {{ parent }}){% endif %} -{% endmacro %} - diff --git a/repondeur/zam_repondeur/templating.py b/repondeur/zam_repondeur/templating.py index e84383dc2..00640dd5b 100644 --- a/repondeur/zam_repondeur/templating.py +++ b/repondeur/zam_repondeur/templating.py @@ -36,6 +36,7 @@ def includeme(config: Configurator) -> None: def add_renderer_globals(event: BeforeRender) -> None: registry = get_current_registry() event["app_name"] = registry.settings["zam.app_name"] + event["contact_email"] = registry.settings["zam.contact_email"] def render_template( diff --git a/repondeur/zam_repondeur/utils.py b/repondeur/zam_repondeur/utils.py index 2e4a7112c..cb2fa30e4 100644 --- a/repondeur/zam_repondeur/utils.py +++ b/repondeur/zam_repondeur/utils.py @@ -7,12 +7,8 @@ from zam_repondeur.models import AVIS -def normalize_num(num: str) -> int: - try: - num_int = int(num) - except ValueError: - num_int = int(num.split("\n")[0].strip(",")) - return num_int +def normalize_num(num: str) -> str: + return num.split("\n", 1)[0].strip(",") def normalize_avis(avis: str) -> str: diff --git a/repondeur/zam_repondeur/views/auth.py b/repondeur/zam_repondeur/views/auth.py index 473ce97cc..b4be8d380 100644 --- a/repondeur/zam_repondeur/views/auth.py +++ b/repondeur/zam_repondeur/views/auth.py @@ -67,7 +67,7 @@ def get(self) -> Any: def next_url(self) -> Any: url = self.request.params.get("source") if url is None or url == self.request.route_url("login"): - url = self.request.resource_url(self.request.root["dossiers"]) + url = self.request.resource_url(self.request.root.self_or_child()) return url @view_config(request_method="POST", require_csrf=True) @@ -117,12 +117,13 @@ def create_auth_token(self, email: str) -> str: def send_auth_token_email(self, token: str, email: str) -> None: app_name = self.request.registry.settings["zam.app_name"] + contact_email = self.request.registry.settings["zam.contact_email"] url = self.request.route_url("auth", _query={"token": token}) - url_dossiers = self.request.resource_url(self.request.root["dossiers"]) + url_home = self.request.resource_url(self.request.root.self_or_child()) mailer = get_mailer(self.request) message = MailMessage( subject=f"Se connecter à {app_name}", - sender="contact@zam.beta.gouv.fr", + sender=contact_email, recipients=[email], body=f""" Bonjour, @@ -137,8 +138,8 @@ def send_auth_token_email(self, token: str, email: str) -> None: Ce lien contient un code personnel à usage unique, valable 10 minutes, pour vous authentifier sur {app_name}. -Une fois connecté·e, vous pourrez directement accéder aux dossiers : -{url_dossiers} +Une fois connecté·e, vous pourrez directement accéder à l’application : +{url_home} Bonne journée ! """.strip(), @@ -226,7 +227,7 @@ def get(self) -> Any: def next_url(self) -> Any: url = self.request.params.get("source") if url is None or url == self.request.route_url("login"): - url = self.request.resource_url(self.request.root["dossiers"]) + url = self.request.resource_url(self.request.root.self_or_child()) return url def log_successful_login_attempt(self, email: str) -> None: @@ -256,7 +257,7 @@ def post(self) -> Any: self.request.user.name = User.normalize_name(name) next_url = self.request.params.get("source") or self.request.resource_url( - self.request.root["dossiers"] + self.request.root.self_or_child() ) return HTTPFound(location=next_url) @@ -294,7 +295,7 @@ def forbidden_view(exception: HTTPForbidden, request: Request) -> Any: ) # Default - message = "L’accès à ce dossier est réservé aux personnes autorisées." + message = "L’accès à cette page est réservé aux personnes autorisées." next_resource = request.root if isinstance(exception.result, ACLDenied): diff --git a/repondeur/zam_repondeur/views/derouleur.py b/repondeur/zam_repondeur/views/derouleur.py new file mode 100644 index 000000000..4932ae069 --- /dev/null +++ b/repondeur/zam_repondeur/views/derouleur.py @@ -0,0 +1,29 @@ +from typing import Any, Dict + +from pyramid.request import Request +from pyramid.view import view_config +from sqlalchemy.orm import joinedload, load_only + +from zam_repondeur.resources import DerouleurCollection + + +@view_config(context=DerouleurCollection, renderer="visionneuse/derouleur.html") +def derouleur(context: DerouleurCollection, request: Request) -> Dict[str, Any]: + lecture = context.lecture_resource.model( + load_only( + "chambre", "dossier_pk", "organe", "partie", "texte_pk", "titre", "phase" + ), + joinedload("articles").options( + load_only("lecture_pk", "mult", "num", "pos", "type", "content"), + joinedload("user_content").load_only("title", "presentation"), + joinedload("amendements") + .joinedload("user_content") + .load_only("objet", "avis", "reponse"), + ), + joinedload("texte").load_only("legislature", "numero"), + ) + articles = sorted(lecture.articles) + return { + "lecture": lecture, + "articles": articles, + } diff --git a/repondeur/zam_repondeur/views/dossier_invite.py b/repondeur/zam_repondeur/views/dossier_invite.py index 6f4351fc9..5f75b3e31 100644 --- a/repondeur/zam_repondeur/views/dossier_invite.py +++ b/repondeur/zam_repondeur/views/dossier_invite.py @@ -136,6 +136,7 @@ def _send_new_users_invitations(self, users: List[User]) -> int: mailer = get_mailer(self.request) reply_to = formataddr((self.request.user.name, self.request.user.email)) app_name = self.request.registry.settings["zam.app_name"] + contact_email = self.request.registry.settings["zam.contact_email"] subject = f"Invitation à rejoindre un dossier législatif sur {app_name}" url = self.request.resource_url(self.request.context) body = f""" @@ -154,7 +155,7 @@ def _send_new_users_invitations(self, users: List[User]) -> int: for user in users: message = MailMessage( subject=subject, - sender="contact@zam.beta.gouv.fr", + sender=contact_email, recipients=[user.email], body=body.strip(), extra_headers={"reply-to": reply_to}, @@ -167,6 +168,7 @@ def _send_existing_users_invitations(self, users: List[User]) -> int: mailer = get_mailer(self.request) reply_to = formataddr((self.request.user.name, self.request.user.email)) app_name = self.request.registry.settings["zam.app_name"] + contact_email = self.request.registry.settings["zam.contact_email"] subject = f"Invitation à participer à un dossier législatif sur {app_name}" url = self.request.resource_url(self.request.context) body = f""" @@ -185,7 +187,7 @@ def _send_existing_users_invitations(self, users: List[User]) -> int: for user in users: message = MailMessage( subject=subject, - sender="contact@zam.beta.gouv.fr", + sender=contact_email, recipients=[user.email], body=body.strip(), extra_headers={"reply-to": reply_to}, @@ -217,7 +219,7 @@ def post(self) -> Response: DossierRetrait.create(dossier=self.dossier, target=target, request=self.request) self.request.session.flash( Message( - cls="success", text=(f"{target} a été retiré·e du dossier avec succès.") + cls="success", text=f"{target} a été retiré·e du dossier avec succès." ) ) return HTTPFound(location=self.request.resource_url(self.context, "retrait")) diff --git a/repondeur/zam_repondeur/views/dossiers_list.py b/repondeur/zam_repondeur/views/dossiers_list.py index 6eec4bd1d..8f770a0e9 100644 --- a/repondeur/zam_repondeur/views/dossiers_list.py +++ b/repondeur/zam_repondeur/views/dossiers_list.py @@ -83,7 +83,7 @@ def post(self) -> Response: self.request.session.flash( Message( cls="success", - text=("Dossier créé avec succès, lectures en cours de création."), + text="Dossier créé avec succès, lectures en cours de création.", ) ) return HTTPFound( diff --git a/repondeur/zam_repondeur/views/download.py b/repondeur/zam_repondeur/views/download.py index ff220f268..68d28f95b 100644 --- a/repondeur/zam_repondeur/views/download.py +++ b/repondeur/zam_repondeur/views/download.py @@ -94,13 +94,14 @@ def download_amendements(context: LectureResource, request: Request) -> Response @view_config(context=LectureResource, name="export_xlsx") def export_xlsx(context: LectureResource, request: Request) -> Response: lecture = context.model(noload("amendements")) - nums, article_param = parse_params(request, lecture=lecture) + nums_params, article_param = parse_params(request, lecture=lecture) if article_param == "all": amendements = ( DBSession.query(Amendement) .join(Article) .filter( - Amendement.lecture == lecture, Amendement.num.in_(nums), # type: ignore + Amendement.lecture == lecture, + Amendement.num.in_(nums_params), # type: ignore ) .options(USER_CONTENT_OPTIONS, LOCATION_OPTIONS) ) @@ -115,7 +116,7 @@ def export_xlsx(context: LectureResource, request: Request) -> Response: Article.num == article_num, Article.mult == article_mult, Article.pos == article_pos, - Amendement.num.in_(nums), # type: ignore + Amendement.num.in_(nums_params), # type: ignore ) .options(USER_CONTENT_OPTIONS, LOCATION_OPTIONS) ) @@ -141,7 +142,7 @@ def export_pdf(context: LectureResource, request: Request) -> Response: DOSSIER_OPTIONS, subqueryload("articles").options(joinedload("user_content")), ) - nums, article_param = parse_params(request, lecture=lecture) + nums_params, article_param = parse_params(request, lecture=lecture) if article_param == "all": article_amendements = ( DBSession.query(Amendement) @@ -165,7 +166,9 @@ def export_pdf(context: LectureResource, request: Request) -> Response: ) amendements = [ - amendement for amendement in article_amendements if amendement.num in nums + amendement + for amendement in article_amendements + if amendement.num in nums_params ] expanded_amendements = list(Batch.expanded_batches(amendements)) @@ -187,13 +190,8 @@ def export_pdf(context: LectureResource, request: Request) -> Response: ) -def parse_params(request: Request, lecture: Lecture) -> Tuple[List[int], str]: - params = request.params.getall("n") - try: - nums: List[int] = [int(num) for num in params] - except ValueError: - raise HTTPBadRequest() - +def parse_params(request: Request, lecture: Lecture) -> Tuple[List[str], str]: + nums_params = request.params.getall("n") total_count_amendements = lecture.nb_amendements max_amendements_for_full_index = int( request.registry.settings.get("zam.limits.max_amendements_for_full_index", 1000) @@ -201,7 +199,7 @@ def parse_params(request: Request, lecture: Lecture) -> Tuple[List[int], str]: too_many_amendements = total_count_amendements > max_amendements_for_full_index default_param = "article.1.." if too_many_amendements else "all" article_param = request.params.get("article", default_param) - return nums, article_param + return nums_params, article_param def write_response( diff --git a/repondeur/zam_repondeur/views/import_csv.py b/repondeur/zam_repondeur/views/import_csv.py index 1c099721b..89d24fd60 100644 --- a/repondeur/zam_repondeur/views/import_csv.py +++ b/repondeur/zam_repondeur/views/import_csv.py @@ -43,7 +43,7 @@ def upload_csv(context: LectureResource, request: Request) -> Response: amendements={ amendement.num: amendement for amendement in lecture.amendements }, - team=context.dossier_resource.dossier.team, + team=lecture.dossier.team, ) except CSVImportError as exc: request.session.flash(Message(cls="danger", text=str(exc))) diff --git a/repondeur/zam_repondeur/views/import_json.py b/repondeur/zam_repondeur/views/import_json.py index 2c8fee032..19ef7e5a1 100644 --- a/repondeur/zam_repondeur/views/import_json.py +++ b/repondeur/zam_repondeur/views/import_json.py @@ -34,7 +34,7 @@ def upload_json(context: LectureResource, request: Request) -> Response: amendement.num: amendement for amendement in lecture.amendements }, articles={article.sort_key_as_str: article for article in lecture.articles}, - team=context.dossier_resource.dossier.team, + team=lecture.dossier.team, ) except ValueError as exc: request.session.flash(Message(cls="danger", text=str(exc))) diff --git a/repondeur/zam_repondeur/views/lecture.py b/repondeur/zam_repondeur/views/lecture.py index f985bd346..0a9e36f5d 100644 --- a/repondeur/zam_repondeur/views/lecture.py +++ b/repondeur/zam_repondeur/views/lecture.py @@ -82,11 +82,7 @@ def progress_status(context: LectureResource, request: Request) -> dict: def search_amendement(context: LectureResource, request: Request) -> dict: lecture = context.model(noload("amendements")) - try: - num_param: str = request.params.get("num", "") - num: int = int(num_param) - except ValueError: - raise HTTPBadRequest() + num: str = request.params.get("num", "") amendement = ( DBSession.query(Amendement) diff --git a/repondeur/zam_repondeur/views/lecture_index.py b/repondeur/zam_repondeur/views/lecture_index.py index cb6a3d9e6..2264146b4 100644 --- a/repondeur/zam_repondeur/views/lecture_index.py +++ b/repondeur/zam_repondeur/views/lecture_index.py @@ -6,7 +6,14 @@ from pyramid.view import view_config from sqlalchemy.orm import joinedload, load_only, subqueryload -from zam_repondeur.models import Amendement, AmendementList, Article, Batch, DBSession +from zam_repondeur.models import ( + Amendement, + AmendementList, + Article, + Batch, + Chambre, + DBSession, +) from zam_repondeur.resources import AmendementCollection, LectureResource AMDTS_OPTIONS = [ @@ -101,6 +108,8 @@ def lecture_index(context: AmendementCollection, request: Request) -> dict: "progress_url": request.resource_url(lecture_resource, "progress_status"), "progress_interval": request.registry.settings["zam.progress.lecture_refresh"], "too_many_amendements": too_many_amendements, + "enter_amendement_url": request.resource_url(context, "saisie"), + "show_help": lecture.chambre in {Chambre.AN, Chambre.SENAT}, } @@ -117,7 +126,7 @@ def get_sort_key(request: Request) -> Callable[[Amendement], tuple]: return sort_by_tri_amendement if tri_amendement_enabled else sort_by_position -def sort_by_tri_amendement(amendement: Amendement) -> Tuple[bool, str, Article, int]: +def sort_by_tri_amendement(amendement: Amendement) -> Tuple[bool, str, Article, str]: return ( amendement.is_abandoned, amendement.tri_amendement or "~", @@ -126,5 +135,5 @@ def sort_by_tri_amendement(amendement: Amendement) -> Tuple[bool, str, Article, ) -def sort_by_position(amendement: Amendement) -> Tuple[bool, int, Article, int]: +def sort_by_position(amendement: Amendement) -> Tuple[bool, int, Article, str]: return amendement.sort_key diff --git a/repondeur/zam_repondeur/views/manage_admins.py b/repondeur/zam_repondeur/views/manage_admins.py index b6618cd87..5752a96e5 100644 --- a/repondeur/zam_repondeur/views/manage_admins.py +++ b/repondeur/zam_repondeur/views/manage_admins.py @@ -51,9 +51,7 @@ def post(self) -> Response: user = DBSession.query(User).filter_by(pk=user_pk).first() AdminRevoke.create(target=user, request=self.request) self.request.session.flash( - Message( - cls="success", text=("Droits d’administration retirés avec succès.") - ) + Message(cls="success", text="Droits d’administration retirés avec succès.") ) return HTTPFound(location=self.request.resource_url(self.context)) @@ -79,9 +77,7 @@ def post(self) -> Response: user = DBSession.query(User).filter_by(pk=user_pk).first() AdminGrant.create(target=user, request=self.request) self.request.session.flash( - Message( - cls="success", text=("Droits d’administration ajoutés avec succès.") - ) + Message(cls="success", text="Droits d’administration ajoutés avec succès.") ) return HTTPFound(location=self.request.resource_url(self.context)) diff --git a/repondeur/zam_repondeur/views/manage_whitelist.py b/repondeur/zam_repondeur/views/manage_whitelist.py index 135b686db..71ea33c90 100644 --- a/repondeur/zam_repondeur/views/manage_whitelist.py +++ b/repondeur/zam_repondeur/views/manage_whitelist.py @@ -53,7 +53,7 @@ def post(self) -> Response: self.request.session.flash( Message( cls="success", - text=("Adresse de courriel ou modèle supprimé(e) avec succès."), + text="Adresse de courriel ou modèle supprimé(e) avec succès.", ) ) return HTTPFound(location=self.request.resource_url(self.context)) @@ -102,7 +102,7 @@ def post(self) -> Response: self.request.session.flash( Message( cls="success", - text=("Adresse de courriel ou modèle créé(e) avec succès."), + text="Adresse de courriel ou modèle créé(e) avec succès.", ) ) return HTTPFound(location=self.request.resource_url(self.context)) diff --git a/repondeur/zam_repondeur/views/tables.py b/repondeur/zam_repondeur/views/tables.py index e8c60ddeb..9fe9ee0a2 100644 --- a/repondeur/zam_repondeur/views/tables.py +++ b/repondeur/zam_repondeur/views/tables.py @@ -162,10 +162,7 @@ def get_target_user_table(self, target: str) -> Optional[UserTable]: target_user = self.request.user else: target_user = DBSession.query(User).filter(User.email == target).one() - if ( - target_user - not in self.context.lecture_resource.dossier_resource.dossier.team.users - ): + if target_user not in self.lecture.dossier.team.users: raise HTTPForbidden("Transfert non autorisé") return target_user.table_for(self.lecture) diff --git a/repondeur/zam_repondeur/visam/__init__.py b/repondeur/zam_repondeur/visam/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/repondeur/zam_repondeur/visam/app.py b/repondeur/zam_repondeur/visam/app.py new file mode 100644 index 000000000..51ed631af --- /dev/null +++ b/repondeur/zam_repondeur/visam/app.py @@ -0,0 +1,55 @@ +from typing import Any + +from paste.deploy.converters import asbool +from pyramid.config import Configurator +from pyramid.router import Router + +from zam_repondeur import BASE_SETTINGS + +from .auth import VisamAuthenticationPolicy +from .resources import VisamRoot + + +def make_app(global_settings: dict, **settings: Any) -> Router: + + settings = {**BASE_SETTINGS, **settings} + + with Configurator(settings=settings) as config: + + config.include("zam_repondeur") + + # Custom app name and contact email address + config.add_settings( + { + "zam.app_name": "Visam", + "zam.contact_email": "contact@visam.beta.gouv.fr", + } + ) + + # Custom logo + config.override_asset( + to_override="zam_repondeur:static/", + override_with="zam_repondeur:visam/static/", + ) + + # Custom templates + config.add_jinja2_search_path("zam_repondeur:visam/templates", name=".html") + + # Customize the resource tree + config.set_root_factory(VisamRoot) + + authn_policy = VisamAuthenticationPolicy( + settings["zam.auth_secret"], + hashalg="sha512", + max_age=int(settings["zam.auth_cookie_duration"]), + secure=asbool(settings["zam.auth_cookie_secure"]), + http_only=True, + ) + config.set_authentication_policy(authn_policy) + + # Scan Visam-specific views + config.scan(".views") + + app = config.make_wsgi_app() + + return app diff --git a/repondeur/zam_repondeur/visam/auth.py b/repondeur/zam_repondeur/visam/auth.py new file mode 100644 index 000000000..37d68e42b --- /dev/null +++ b/repondeur/zam_repondeur/visam/auth.py @@ -0,0 +1,15 @@ +from typing import List + +from pyramid.request import Request + +from zam_repondeur.auth import AuthenticationPolicy + + +class VisamAuthenticationPolicy(AuthenticationPolicy): + def effective_principals(self, request: Request) -> List[str]: + """Add chambre-related groups specific to Visam.""" + principals = super().effective_principals(request) + if request.user is not None: + for chambre in request.user.chambres: + principals.append(f"chambre:{chambre.name}") + return principals diff --git a/repondeur/zam_repondeur/visam/models/__init__.py b/repondeur/zam_repondeur/visam/models/__init__.py new file mode 100644 index 000000000..f86855830 --- /dev/null +++ b/repondeur/zam_repondeur/visam/models/__init__.py @@ -0,0 +1,5 @@ +from .events.amendements import AmendementSaisi # noqa +from .events.membership import MembershipAdded, MembershipRemoved # noqa +from .membership import UserMembership # noqa +from .organisation import Organisation # noqa +from .seance import Formation, Seance # noqa diff --git a/repondeur/zam_repondeur/visam/models/events/__init__.py b/repondeur/zam_repondeur/visam/models/events/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/repondeur/zam_repondeur/visam/models/events/amendements.py b/repondeur/zam_repondeur/visam/models/events/amendements.py new file mode 100644 index 000000000..1c733ffcf --- /dev/null +++ b/repondeur/zam_repondeur/visam/models/events/amendements.py @@ -0,0 +1,19 @@ +from string import Template + +from zam_repondeur.models.events.amendement import AmendementEvent + + +class AmendementSaisi(AmendementEvent): + __mapper_args__ = {"polymorphic_identity": "amendement_saisi"} + + icon = "edit" + + summary_template = Template( + f"L’amendement a été saisi par $user." + ) + + def apply(self) -> None: + pass + + def render_details(self) -> str: + return "" diff --git a/repondeur/zam_repondeur/visam/models/events/membership.py b/repondeur/zam_repondeur/visam/models/events/membership.py new file mode 100644 index 000000000..c75ed1d4b --- /dev/null +++ b/repondeur/zam_repondeur/visam/models/events/membership.py @@ -0,0 +1,103 @@ +from string import Template +from typing import Any, Optional + +from jinja2 import Markup +from pyramid.request import Request + +from zam_repondeur.models.base import DBSession +from zam_repondeur.models.chambre import Chambre +from zam_repondeur.models.events.base import Event +from zam_repondeur.models.users import User + +from ..membership import UserMembership +from ..organisation import Organisation + + +class MembershipEvent(Event): + details_template = Template("") + + @property + def template_vars(self) -> dict: + template_vars = { + "target_user": self.data["target_user"], + "target_chambre": self.data["target_chambre"], + "target_organisation": self.data["target_organisation"], + } + if self.user: + template_vars.update({"user": self.user.name, "email": self.user.email}) + return template_vars + + def render_summary(self) -> str: + return Markup(self.summary_template.safe_substitute(**self.template_vars)) + + def render_details(self) -> str: + return Markup(self.details_template.safe_substitute(**self.template_vars)) + + +class MembershipAdded(MembershipEvent): + __mapper_args__ = {"polymorphic_identity": "membership_added"} + icon = "edit" + + def __init__( + self, + target_user: User, + target_chambre: Chambre, + target_organisation: Organisation, + comment: Optional[str], + request: Optional[Request] = None, + **kwargs: Any, + ): + kwargs["target_user"] = str(target_user) + kwargs["target_chambre"] = target_chambre.value + kwargs["target_organisation"] = target_organisation.name + super().__init__(request=request, **kwargs) + self.target_user = target_user + self.target_chambre = target_chambre + self.target_organisation = target_organisation + + @property + def summary_template(self) -> Template: + if self.user: + who = "$user" + else: + who = "L’équipe technique" + return Template( + f"{who} a ajouté $target_user - $target_organisation au $target_chambre." + ) + + def apply(self) -> None: + UserMembership.create( + user=self.target_user, + chambre=self.target_chambre, + organisation=self.target_organisation, + ) + + +class MembershipRemoved(MembershipEvent): + __mapper_args__ = {"polymorphic_identity": "membership_removed"} + icon = "edit" + + def __init__( + self, + membership: UserMembership, + request: Optional[Request] = None, + **kwargs: Any, + ): + kwargs["target_user"] = str(membership.user) + kwargs["target_chambre"] = membership.chambre.value + kwargs["target_organisation"] = membership.organisation.name + super().__init__(request=request, **kwargs) + self.membership = membership + + @property + def summary_template(self) -> Template: + if self.user: + who = "$user" + else: + who = "L’équipe technique" + return Template( + f"{who} a retiré $target_user - $target_organisation du $target_chambre." + ) + + def apply(self) -> None: + DBSession.delete(self.membership) diff --git a/repondeur/zam_repondeur/visam/models/membership.py b/repondeur/zam_repondeur/visam/models/membership.py new file mode 100644 index 000000000..fe30cfb8b --- /dev/null +++ b/repondeur/zam_repondeur/visam/models/membership.py @@ -0,0 +1,77 @@ +from typing import Optional + +from more_itertools import first_true +from sqlalchemy import Column, Enum, ForeignKey, Integer +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.orm import backref, relationship + +from zam_repondeur.models.base import Base, DBSession +from zam_repondeur.models.chambre import Chambre +from zam_repondeur.models.users import User + +from .organisation import Organisation + + +class UserMembership(Base): + """ + Association object + + https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html#association-object + """ + + __tablename__ = "users_chambres" + + __repr_keys__ = ("user", "chambre", "organisation") + + user_pk = Column( + Integer, + ForeignKey("users.pk", onupdate="CASCADE", ondelete="CASCADE"), + primary_key=True, + ) + user = relationship( + User, backref=backref("memberships", cascade="all, delete-orphan") + ) + + chambre = Column( + Enum(Chambre), + nullable=False, + primary_key=True, + doc="""Le conseil concerné (par exemple: CCFP, CSFPE...).""", + ) + + organisation_pk = Column( + Integer, ForeignKey("organisations.pk"), primary_key=True, nullable=False, + ) + organisation: Organisation = relationship( + Organisation, backref=backref("memberships") + ) + + @classmethod + def create( + cls, user: User, chambre: Chambre, organisation: Organisation + ) -> "UserMembership": + user_membership = cls(user=user, chambre=chambre, organisation=organisation) + DBSession.add(user_membership) + return user_membership + + +def _membership_of(self: User, chambre: Chambre) -> Optional[UserMembership]: + return first_true(self.memberships, pred=lambda m: m.chambre == chambre) + + +User.membership_of = _membership_of + + +User.chambres = association_proxy( + "memberships", "chambre", creator=lambda user: UserMembership(user=user) +) + + +User.organisations = association_proxy( + "memberships", "organisation", creator=lambda user: UserMembership(user=user) +) + + +Organisation.members = association_proxy( + "memberships", "user", creator=lambda user: UserMembership(user=user) +) diff --git a/repondeur/zam_repondeur/visam/models/organisation.py b/repondeur/zam_repondeur/visam/models/organisation.py new file mode 100644 index 000000000..b466d4a75 --- /dev/null +++ b/repondeur/zam_repondeur/visam/models/organisation.py @@ -0,0 +1,34 @@ +from typing import Optional + +from sqlalchemy import Column, Integer, Text + +from zam_repondeur.models.base import Base, DBSession + + +class Organisation(Base): + __tablename__ = "organisations" + + __repr_keys__ = ("name",) + + pk = Column(Integer, primary_key=True) + name: str = Column(Text, nullable=False, unique=True) + + def __str__(self) -> str: + return self.name + + @classmethod + def create(cls, name: str) -> "Organisation": + organisation = cls(name=name) + DBSession.add(organisation) + return organisation + + @property + def is_gouvernement(self) -> bool: + return self.name == "Gouvernement" + + @classmethod + def find_by_name(cls, name: str) -> Optional["Organisation"]: + organisation: Optional[Organisation] = DBSession.query(cls).filter_by( + name=name + ).one_or_none() + return organisation diff --git a/repondeur/zam_repondeur/visam/models/seance.py b/repondeur/zam_repondeur/visam/models/seance.py new file mode 100644 index 000000000..a0f8d7e69 --- /dev/null +++ b/repondeur/zam_repondeur/visam/models/seance.py @@ -0,0 +1,145 @@ +import datetime +import enum +from typing import Any, List, Optional + +from sqlalchemy import Boolean, CheckConstraint, Column, Date, Enum, ForeignKey, Integer +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.orderinglist import ordering_list +from sqlalchemy.orm import relationship + +from zam_repondeur.models import get_one_or_create +from zam_repondeur.models.base import Base, DBSession +from zam_repondeur.models.chambre import Chambre +from zam_repondeur.models.lecture import Lecture +from zam_repondeur.models.users import Team, User + + +class SeanceLecture(Base): + """ + Association object + + https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html#association-object + """ + + __tablename__ = "seances_lectures" + + seance_pk = Column( + Integer, + ForeignKey("seances.pk", onupdate="CASCADE", ondelete="CASCADE"), + primary_key=True, + ) + lecture_pk = Column( + Integer, + ForeignKey("lectures.pk", onupdate="CASCADE", ondelete="CASCADE"), + primary_key=True, + ) + lecture = relationship("Lecture") + + position = Column(Integer, doc="Ordre des lectures dans un conseil") + + +class Formation(enum.Enum): + ASSEMBLEE_PLENIERE = "Assemblée plénière" + FORMATION_SPECIALISEE = "Formation spécialisée" + + +class Seance(Base): + """ + Une séance d'un conseil + """ + + __tablename__ = "seances" + __table_args__ = (CheckConstraint("chambre NOT IN ('AN', 'SENAT')"),) + + pk = Column(Integer, primary_key=True) + chambre = Column( + Enum(Chambre), + nullable=False, + doc=""" + Le conseil concerné (par exemple: CCFP, CSFPE...). + """, + ) + date = Column( + Date, + nullable=False, + doc=""" + La date de la séance du conseil. + """, + ) + formation = Column( + Enum(Formation), + nullable=False, + doc=""" + Le conseil peut se réunir en assemblée plénière (le cas le plus courant), + ou en formation spécialisée. + """, + ) + urgence_declaree = Column( + Boolean, + nullable=False, + doc=""" + Si le gouvernement déclare l’urgence, alors les délais seront réduits. + """, + ) + + team_pk = Column(Integer, ForeignKey("teams.pk"), nullable=False) + team = relationship("Team") + + # We use `ordering_list` to automatically map the order of the list + # to the `position` attribute on the association object. + # https://docs.sqlalchemy.org/en/13/orm/extensions/orderinglist.html#module-sqlalchemy.ext.orderinglist + _lectures: List[SeanceLecture] = relationship( + "SeanceLecture", + order_by=[SeanceLecture.position], + collection_class=ordering_list("position"), + cascade="all, delete-orphan", + ) + + # We use `association_proxy` to hide the intermediate association objects. + # https://docs.sqlalchemy.org/en/13/orm/extensions/associationproxy.html#module-sqlalchemy.ext.associationproxy + lectures: List[Lecture] = association_proxy( + "_lectures", "lecture", creator=lambda lecture: SeanceLecture(lecture=lecture) + ) + + def __repr__(self) -> str: + return f"{self.chambre.name} du {self.date.strftime('%x')}" + + @property + def slug(self) -> str: + return f"{self.chambre.name.lower()}-{self.date}" + + @classmethod + def create( + cls, + chambre: Chambre, + formation: Formation, + date: datetime.date, + urgence_declaree: bool = False, + ) -> "Seance": + if chambre in {Chambre.AN, Chambre.SENAT}: + raise ValueError("Chambre invalide") + seance = cls( + chambre=chambre, + formation=formation, + date=date, + urgence_declaree=urgence_declaree, + ) + team, _ = get_one_or_create(Team, name=seance.slug) + seance.team = team + DBSession.add(seance) + for admin in DBSession.query(User).filter( + User.admin_at.isnot(None) # type: ignore + ): + admin.teams.append(team) + return seance + + @classmethod + def get(cls, slug: str, *options: Any) -> Optional["Seance"]: + try: + chambre, date = slug.split("-", 1) + except ValueError: + return None + res: Optional["Seance"] = DBSession.query(cls).filter( + cls.chambre == chambre.upper(), cls.date == date, + ).options(*options).first() + return res diff --git a/repondeur/zam_repondeur/visam/resources.py b/repondeur/zam_repondeur/visam/resources.py new file mode 100644 index 000000000..9432e6a9c --- /dev/null +++ b/repondeur/zam_repondeur/visam/resources.py @@ -0,0 +1,172 @@ +from typing import Any, List, Optional, cast + +from pyramid.request import Request +from pyramid.security import Allow, Authenticated, Deny, Everyone +from sqlalchemy import desc +from sqlalchemy.orm import Query + +from zam_repondeur.menu import MenuAction +from zam_repondeur.models import Chambre, DBSession, Dossier, Lecture, User +from zam_repondeur.resources import ( + ACE, + AmendementCollection, + ArticleCollection, + DerouleurCollection, + DossierResource, + LectureResource, + Resource, + ResourceNotFound, + Root, + SharedTableCollection, + TableCollection, +) + +from .models.events.membership import MembershipEvent +from .models.seance import Seance, SeanceLecture + + +class VisamRoot(Root): + """ + We create a special resource tree for Visam, with seances instead of dossiers. + """ + + def __init__(self, _request: Request) -> None: + super().__init__(_request) + del self["dossiers"] + self.add_child(SeanceCollection(name="seances", parent=self)) + self.add_child(MembersCollection(name="members", parent=self)) + + @property + def default_child(self) -> Optional[Resource]: + return cast(Resource, self["seances"]) + + class ManageMembers(MenuAction): + title = "Gestion des membres" + tab_name = "members" + + @property + def should_show(self) -> bool: + return self.request.has_permission("manage", self.resource["members"]) + + @property + def url(self) -> str: + return self.request.resource_url(self.resource["members"]) + + menu_actions = Root.menu_actions + [ManageMembers] + + +class SeanceCollection(Resource): + __acl__ = [ + (Allow, "group:admins", "create_seance"), + (Deny, Everyone, "create_seance"), + ] + + def models( + self, *options: Any, chambres: Optional[List[Chambre]] = None + ) -> List[Seance]: + query: Query = DBSession.query(Seance) + if chambres: + query = query.filter(Seance.chambre.in_(chambres)) + query = query.order_by(Seance.date.desc()).options(*options) + return cast(List[Seance], query.all()) + + def __getitem__(self, key: str) -> Resource: + resource = SeanceResource(name=key, parent=self) + try: + resource.model() + except ResourceNotFound: + raise KeyError + return resource + + +class SeanceResource(Resource): + def __acl__(self) -> List[ACE]: + # Only chambre members and admins can view it. + return [ + (Allow, "group:admins", "view"), + (Allow, f"chambre:{self.model().chambre.name}", "view"), + (Deny, Authenticated, "view"), + ] + + def __init__(self, name: str, parent: Resource) -> None: + super().__init__(name=name, parent=parent) + self.slug = name + self.add_child(TexteCollection(name="textes", parent=self)) + + @property + def parent(self) -> SeanceCollection: + return cast(SeanceCollection, self.__parent__) + + def model(self, *options: Any) -> Seance: + seance = Seance.get(self.slug, *options) + if seance is None: + raise ResourceNotFound(self) + return seance + + @property + def breadcrumbs_label(self) -> Optional[str]: + seance: Seance = self.model() + return str(seance) + + +class TexteCollection(Resource): + @property + def parent(self) -> SeanceResource: + return cast(SeanceResource, self.__parent__) + + def __getitem__(self, key: str) -> Resource: + return TexteResource(slug=key, parent=self) + + +class TexteResource(LectureResource): + """ + We inherit from LectureResource in order to reuse the associated views + """ + + def __init__(self, slug: str, parent: Resource) -> None: + Resource.__init__(self, name=slug, parent=parent) + self.slug = slug + self.add_child(AmendementCollection(name="amendements", parent=self)) + self.add_child(ArticleCollection(name="articles", parent=self)) + self.add_child(TableCollection(name="tables", parent=self)) + self.add_child(SharedTableCollection(name="boites", parent=self)) + self.add_child(DerouleurCollection(name="derouleur", parent=self)) + + def model(self, *options: Any) -> Lecture: + seance = self.parent.parent.model() + lecture: Optional[Lecture] = ( + DBSession.query(Lecture) + .join(SeanceLecture) + .join(Dossier) + .filter(SeanceLecture.seance_pk == seance.pk, Dossier.slug == self.slug) + .options(*options) + ).one_or_none() + if lecture is None: + raise ResourceNotFound(self) + return lecture + + @property + def dossier_resource(self) -> Optional[DossierResource]: + return None + + @property + def breadcrumbs_label(self) -> Optional[str]: + lecture: Lecture = self.model() + return lecture.dossier.titre + + def back_resource(self, request: Request) -> Optional["Resource"]: + seance = self.parent.parent + return seance + + +class MembersCollection(Resource): + __acl__ = [(Allow, "group:admins", "manage"), (Deny, Everyone, "manage")] + + def models(self, *options: Any) -> List[User]: + result: List[User] = DBSession.query(User).options(*options) + return result + + def events(self) -> Query: + return DBSession.query(MembershipEvent).order_by( + desc(MembershipEvent.created_at) + ) diff --git a/repondeur/zam_repondeur/visam/static/css/custom.css b/repondeur/zam_repondeur/visam/static/css/custom.css new file mode 100644 index 000000000..d6e494f26 --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/css/custom.css @@ -0,0 +1,4 @@ +nav.main { + background-image: url("../img/logo_visam_white_v_only.svg"); + background-size: 2.5rem; +} diff --git a/repondeur/zam_repondeur/visam/static/favicons/android-chrome-192x192.png b/repondeur/zam_repondeur/visam/static/favicons/android-chrome-192x192.png new file mode 100644 index 000000000..3a821d00d Binary files /dev/null and b/repondeur/zam_repondeur/visam/static/favicons/android-chrome-192x192.png differ diff --git a/repondeur/zam_repondeur/visam/static/favicons/android-chrome-512x512.png b/repondeur/zam_repondeur/visam/static/favicons/android-chrome-512x512.png new file mode 100644 index 000000000..dc04096a4 Binary files /dev/null and b/repondeur/zam_repondeur/visam/static/favicons/android-chrome-512x512.png differ diff --git a/repondeur/zam_repondeur/visam/static/favicons/apple-touch-icon.png b/repondeur/zam_repondeur/visam/static/favicons/apple-touch-icon.png new file mode 100644 index 000000000..22f26c191 Binary files /dev/null and b/repondeur/zam_repondeur/visam/static/favicons/apple-touch-icon.png differ diff --git a/repondeur/zam_repondeur/visam/static/favicons/browserconfig.xml b/repondeur/zam_repondeur/visam/static/favicons/browserconfig.xml new file mode 100644 index 000000000..b3930d0f0 --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/repondeur/zam_repondeur/visam/static/favicons/favicon-16x16.png b/repondeur/zam_repondeur/visam/static/favicons/favicon-16x16.png new file mode 100644 index 000000000..a250eb78e Binary files /dev/null and b/repondeur/zam_repondeur/visam/static/favicons/favicon-16x16.png differ diff --git a/repondeur/zam_repondeur/visam/static/favicons/favicon-32x32.png b/repondeur/zam_repondeur/visam/static/favicons/favicon-32x32.png new file mode 100644 index 000000000..851db5ba6 Binary files /dev/null and b/repondeur/zam_repondeur/visam/static/favicons/favicon-32x32.png differ diff --git a/repondeur/zam_repondeur/visam/static/favicons/favicon.ico b/repondeur/zam_repondeur/visam/static/favicons/favicon.ico new file mode 100644 index 000000000..6aa13b17d Binary files /dev/null and b/repondeur/zam_repondeur/visam/static/favicons/favicon.ico differ diff --git a/repondeur/zam_repondeur/visam/static/favicons/mstile-150x150.png b/repondeur/zam_repondeur/visam/static/favicons/mstile-150x150.png new file mode 100644 index 000000000..4502d8771 Binary files /dev/null and b/repondeur/zam_repondeur/visam/static/favicons/mstile-150x150.png differ diff --git a/repondeur/zam_repondeur/visam/static/favicons/safari-pinned-tab.svg b/repondeur/zam_repondeur/visam/static/favicons/safari-pinned-tab.svg new file mode 100644 index 000000000..d3469cdf4 --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/favicons/safari-pinned-tab.svg @@ -0,0 +1,44 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/repondeur/zam_repondeur/visam/static/favicons/site.webmanifest b/repondeur/zam_repondeur/visam/static/favicons/site.webmanifest new file mode 100644 index 000000000..b20abb7cb --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/favicons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/repondeur/zam_repondeur/visam/static/img/draganddrop.svg b/repondeur/zam_repondeur/visam/static/img/draganddrop.svg new file mode 100644 index 000000000..7047b77f8 --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/img/draganddrop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/repondeur/zam_repondeur/visam/static/img/logo_visam_black_v_only.svg b/repondeur/zam_repondeur/visam/static/img/logo_visam_black_v_only.svg new file mode 100644 index 000000000..b5ad10965 --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/img/logo_visam_black_v_only.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/repondeur/zam_repondeur/visam/static/img/logo_visam_blue_v_only.svg b/repondeur/zam_repondeur/visam/static/img/logo_visam_blue_v_only.svg new file mode 100644 index 000000000..25d7b544c --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/img/logo_visam_blue_v_only.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/repondeur/zam_repondeur/visam/static/img/logo_visam_white_v_only.svg b/repondeur/zam_repondeur/visam/static/img/logo_visam_white_v_only.svg new file mode 100644 index 000000000..bab918382 --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/img/logo_visam_white_v_only.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/repondeur/zam_repondeur/visam/static/js/amendements-order.js b/repondeur/zam_repondeur/visam/static/js/amendements-order.js new file mode 100644 index 000000000..dbaf6727c --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/js/amendements-order.js @@ -0,0 +1,81 @@ +class AmendementsOrder extends Stimulus.Controller { + static get targets() { + return ['parent', 'dropzone', 'amendement'] + } + + dragging(event) { + // Necessary to start the dragging stuff. + } + registerDragged(event) { + this.dragged = event.target + this.draggedPreviousDropzone = event.target.previousElementSibling + this.draggedNextDropzone = event.target.nextElementSibling + } + preventDefault(event) { + event.preventDefault() + } + + // Dropzones management. + showDropzones(event) { + this.dropzoneTargets.forEach(dropzone => { + dropzone.classList.add('highlighted') + }) + this.draggedPreviousDropzone.classList.remove('highlighted') + this.draggedNextDropzone.classList.remove('highlighted') + this.dragged.classList.add('highlighted') + this.dragged.style.cursor = 'row-resize' + } + highlightDropzone(event) { + if (event.target.parentElement.className.includes('dropzone')) { + event.target.parentElement.classList.add('hover') + } + } + downlightDropzone(event) { + if (event.target.parentElement.className.includes('dropzone')) { + event.target.parentElement.classList.remove('hover') + } + } + hideDropzones(event) { + this.dropzoneTargets.forEach(dropzone => { + dropzone.classList.remove('highlighted') + dropzone.classList.remove('hover') + }) + this.dragged.classList.remove('highlighted') + this.dragged.style.cursor = 'auto' + } + + // Actual reordering on drop. + reorder(event) { + const nextSibling = event.target.parentElement.nextElementSibling + this.parentTarget.removeChild(this.dragged) + this.parentTarget.removeChild(this.draggedNextDropzone) + const newDragged = this.parentTarget.insertBefore(this.dragged, nextSibling) + this.parentTarget.insertBefore(this.draggedNextDropzone, nextSibling) + const newOrder = this.amendementTargets.map( + amendement => amendement.dataset.num + ) + this.submitOrder(newOrder) + } + + XHROptions() { + const headers = new Headers({ + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json' + }) + return { + method: 'POST', + credentials: 'include', + headers: headers + } + } + + submitOrder(order) { + const options = this.XHROptions() + options['body'] = JSON.stringify({ + order: order + }) + fetch(this.data.get('url'), options) + } +} + +application.register('amendements-order', AmendementsOrder) diff --git a/repondeur/zam_repondeur/visam/static/js/members.js b/repondeur/zam_repondeur/visam/static/js/members.js new file mode 100644 index 000000000..8539eec39 --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/js/members.js @@ -0,0 +1,17 @@ +application.register( + 'members', + class extends Stimulus.Controller { + delete(event) { + if ( + window.confirm( + 'Êtes-vous sûr·e de vouloir retirer cette personne de ce type de conseil ? ' + + '(il ne pourra plus accéder à aucune séance en cours ou à venir)' + ) + ) { + return + } else { + event.preventDefault() + } + } + } +) diff --git a/repondeur/zam_repondeur/visam/static/js/seance.js b/repondeur/zam_repondeur/visam/static/js/seance.js new file mode 100644 index 000000000..2656662cc --- /dev/null +++ b/repondeur/zam_repondeur/visam/static/js/seance.js @@ -0,0 +1,79 @@ +class TextesOrder extends Stimulus.Controller { + static get targets() { + return ['parent', 'dropzone', 'texte'] + } + + dragging(event) { + // Necessary to start the dragging stuff. + } + registerDragged(event) { + this.dragged = event.target + this.draggedPreviousDropzone = event.target.previousElementSibling + this.draggedNextDropzone = event.target.nextElementSibling + } + preventDefault(event) { + event.preventDefault() + } + + // Dropzones management. + showDropzones(event) { + this.dropzoneTargets.forEach(dropzone => { + dropzone.classList.add('highlighted') + }) + this.draggedPreviousDropzone.classList.remove('highlighted') + this.draggedNextDropzone.classList.remove('highlighted') + this.dragged.classList.add('highlighted') + this.dragged.style.cursor = 'row-resize' + } + highlightDropzone(event) { + if (event.target.className.includes('dropzone')) { + event.target.classList.add('hover') + } + } + downlightDropzone(event) { + if (event.target.className.includes('dropzone')) { + event.target.classList.remove('hover') + } + } + hideDropzones(event) { + this.dropzoneTargets.forEach(dropzone => { + dropzone.classList.remove('highlighted') + dropzone.classList.remove('hover') + }) + this.dragged.classList.remove('highlighted') + this.dragged.style.cursor = 'auto' + } + + // Actual reordering on drop. + reorder(event) { + const nextSibling = event.target.nextElementSibling + this.parentTarget.removeChild(this.dragged) + this.parentTarget.removeChild(this.draggedNextDropzone) + const newDragged = this.parentTarget.insertBefore(this.dragged, nextSibling) + this.parentTarget.insertBefore(this.draggedNextDropzone, nextSibling) + const newOrder = this.texteTargets.map(texte => texte.dataset.pk) + this.submitOrder(newOrder) + } + + XHROptions() { + const headers = new Headers({ + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json' + }) + return { + method: 'POST', + credentials: 'include', + headers: headers + } + } + + submitOrder(order) { + const options = this.XHROptions() + options['body'] = JSON.stringify({ + order: order + }) + fetch(this.data.get('url'), options) + } +} + +application.register('textes-order', TextesOrder) diff --git a/repondeur/zam_repondeur/visam/templates/amendements_add.html b/repondeur/zam_repondeur/visam/templates/amendements_add.html new file mode 100644 index 000000000..75867baaf --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/amendements_add.html @@ -0,0 +1,190 @@ +{% extends "_base_lecture.html" %} + +{% block header %} + {{ super() }} + + +{% endblock %} + +{% block body %} +

Saisir un nouvel amendement

+
+
+

{{ lecture.dossier.titre }}

+

{{ lecture }}

+
+ + +
+
+
+
+ {% if can_select_organisation %} +
+ + +
+ {% endif %} +
+ + +
+
+ + +
+
+ +
+
+
+{% endblock %} + + +{% block scripts %} + + + +{% endblock %} diff --git a/repondeur/zam_repondeur/visam/templates/article_preview.html b/repondeur/zam_repondeur/visam/templates/article_preview.html new file mode 100644 index 000000000..2148c4b08 --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/article_preview.html @@ -0,0 +1,3 @@ +{% from "article_macros.html" import content %} + +{{ content(article) }} diff --git a/repondeur/zam_repondeur/visam/templates/members_journal.html b/repondeur/zam_repondeur/visam/templates/members_journal.html new file mode 100644 index 000000000..987ee4fe8 --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/members_journal.html @@ -0,0 +1,20 @@ +{% extends "_base.html" %} +{% import "macros.html" as macros %} + +{% block main_class %}box{% endblock %} + +{% block header %} + + +{% endblock %} + +{% block body %} +

Journal

+ {{ macros.timeline(events, today) }} +{% endblock %} diff --git a/repondeur/zam_repondeur/visam/templates/members_list.html b/repondeur/zam_repondeur/visam/templates/members_list.html new file mode 100644 index 000000000..e03a5b020 --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/members_list.html @@ -0,0 +1,129 @@ +{% extends "_base.html" %} +{% import "macros.html" as macros %} + +{% block header %} + +{% endblock %} + +{% block body %} +
+

Utilisateur·ice·s

+ + {% if last_event_datetime %} + + {% endif %} + + + + + + {% for chambre in chambres %} + + {% endfor %} + + + {% for user in users %} + + + + {% for chambre in chambres %} + {% set membership = user.membership_of(chambre) %} + + {% endfor %} + + {% endfor %} + +
Utilisateur·iceAdministrateur·ice{{ chambre.name }}
{{ user }} + {% if user.is_admin %} + ✅ + {% else %} + — + {% endif %} + + {% if membership %} + {{ membership.organisation }} +
+ + + +
+ +
+
+ {% else %} + — +
+ + + +
+ +
+
+ {% endif %} +
+
+
+ Retour +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/repondeur/zam_repondeur/visam/templates/seance_add_texte.html b/repondeur/zam_repondeur/visam/templates/seance_add_texte.html new file mode 100644 index 000000000..ad56c6eae --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/seance_add_texte.html @@ -0,0 +1,59 @@ +{% extends "_base.html" %} + +{% block header %} + +{% endblock %} + +{% block body %} +
+

Ajouter un texte

+ +
+ + +
+ +
+ + +
+ +
+ Retour + +
+ +
+{% endblock %} + + +{% block scripts %} + + +{% endblock %} diff --git a/repondeur/zam_repondeur/visam/templates/seance_item.html b/repondeur/zam_repondeur/visam/templates/seance_item.html new file mode 100644 index 000000000..c53946cec --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/seance_item.html @@ -0,0 +1,142 @@ +{% extends "_base.html" %} +{% import "macros.html" as macros %} + +{% block header %} + +{% endblock %} + +{% block main_class %}{% endblock %} + +{% block body %} +
+

{{ seance }}

+
+ {% for lecture in lectures %} +
+

{{ lecture.dossier.titre }}

+ +
+
+ {% else %} +
+

Aucun texte pour l’instant.

+
+ {% endfor %} +
+ +{% endblock %} + +{% block scripts %} + {% if lectures | length > 1 %} + + {% endif %} +{% endblock %} diff --git a/repondeur/zam_repondeur/visam/templates/seances_add.html b/repondeur/zam_repondeur/visam/templates/seances_add.html new file mode 100644 index 000000000..7082947cf --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/seances_add.html @@ -0,0 +1,61 @@ +{% extends "_base.html" %} + +{% block header %} + +{% endblock %} + +{% block body %} +
+

Ajouter une séance d’un conseil

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ Retour + +
+ +
+{% endblock %} diff --git a/repondeur/zam_repondeur/visam/templates/seances_list.html b/repondeur/zam_repondeur/visam/templates/seances_list.html new file mode 100644 index 000000000..6a9829cdc --- /dev/null +++ b/repondeur/zam_repondeur/visam/templates/seances_list.html @@ -0,0 +1,103 @@ +{% extends "_base.html" %} +{% import "macros.html" as macros %} + +{% block header %} + +{% endblock %} + +{% block main_class %}{% endblock %} + +{% block body %} +
+

Séances de conseils

+ {% for seance in seances %} +
+
+

{{ seance.chambre.name }} du {{ seance.date.strftime("%x") }}

+

{{ seance.chambre.value }} — {{ seance.formation.value }}

+
+ +
+ {% else %} +
+

Aucune séance pour l’instant.

+
+ {% endfor %} +
+ {% if can_create_seance %} + + {% endif %} +{% endblock %} diff --git a/repondeur/zam_repondeur/visam/views/__init__.py b/repondeur/zam_repondeur/visam/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/repondeur/zam_repondeur/visam/views/amendements_add.py b/repondeur/zam_repondeur/visam/views/amendements_add.py new file mode 100644 index 000000000..ee172b82c --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/amendements_add.py @@ -0,0 +1,137 @@ +from typing import Optional + +from pyramid.httpexceptions import HTTPBadRequest, HTTPFound +from pyramid.request import Request +from pyramid.response import Response +from pyramid.view import view_config, view_defaults + +from zam_repondeur.message import Message +from zam_repondeur.models import Amendement, Article, DBSession, Lecture +from zam_repondeur.models.division import SubDiv +from zam_repondeur.resources import AmendementCollection +from zam_repondeur.services.clean import clean_html +from zam_repondeur.visam.models import AmendementSaisi, Organisation, UserMembership + + +@view_defaults(context=AmendementCollection, name="saisie") +class AddAmendementView: + def __init__(self, context: AmendementCollection, request: Request): + self.context = context + self.request = request + self.lecture: Lecture = self.context.parent.model() + self.membership: Optional[UserMembership] = self.request.user.membership_of( + self.lecture.chambre + ) + + @view_config(request_method="GET", renderer="amendements_add.html") + def get(self) -> dict: + lecture_resource = self.context.parent + return { + "lecture_resource": lecture_resource, + "dossier_resource": lecture_resource.dossier_resource, + "current_tab": "saisie-amendement", + "lecture": self.lecture, + "can_select_organisation": self.can_select_organisation(), + "gouvernement": Organisation.find_by_name("Gouvernement"), + "organisations_except_gouvernement": DBSession.query(Organisation) + .filter(Organisation.name != "Gouvernement") + .all(), + "subdivs": [ + (article.url_key, str(article)) + for article in sorted(self.lecture.articles) + ], + "article_collection_url": self.request.resource_path( + lecture_resource["articles"] + ), + } + + @view_config(request_method="POST") + def post(self) -> Response: + organisation = self.organisation() + num = self.next_num_for(organisation.name) + corps = clean_html(self.request.POST.get("corps", "")) + expose = clean_html(self.request.POST.get("expose", "")) + + # Find article + subdiv = SubDiv(*self.request.POST.get("subdiv").split(".")) + article = ( + DBSession.query(Article) + .filter_by( + lecture=self.lecture, + type=subdiv.type_, + num=subdiv.num, + mult=subdiv.mult, + pos=subdiv.pos, + ) + .one_or_none() + ) + + auteur = Amendement.AUTEUR_GOUVERNEMENT if organisation.is_gouvernement else "" + groupe = organisation.name + + max_position = max( + ( + amdt.position + for amdt in self.lecture.amendements + if amdt.position is not None + ), + default=0, + ) + + # Create amendement + amendement = Amendement.create( + lecture=self.lecture, + article=article, + num=num, + auteur=auteur, + groupe=groupe, + corps=corps, + expose=expose, + position=max_position + 1, + ) + + # Add initial event to journal + AmendementSaisi.create(amendement=amendement, request=self.request) + + # Show success notification + self.request.session.flash( + Message(cls="success", text="Amendement saisi avec succès.") + ) + + # Redirect to index + index_url = self.request.resource_url(self.context, anchor=amendement.slug) + return HTTPFound(location=index_url) + + def organisation(self) -> Organisation: + # Get the organisation from the submitted form + if self.can_select_organisation(): + organisation = Organisation.find_by_name( + self.request.POST.get("organisation") + ) + if organisation is None: + raise HTTPBadRequest("Invalid organisation") + return organisation + + # Get the organisation from the user's membership + if self.membership is not None: + return self.membership.organisation + + raise HTTPBadRequest("Cannot determine organisation") + + def next_num_for(self, groupe_title: str) -> str: + nums = ( + DBSession.query(Amendement.num) + .filter( + Amendement.lecture == self.lecture, + Amendement.num.ilike(groupe_title + " %"), # type: ignore + ) + .all() + ) + start = len(groupe_title) + 1 + max_num = max((int(num[start:]) for (num,) in nums), default=0) + return groupe_title + " " + str(max_num + 1) + + def can_select_organisation(self) -> bool: + return self.request.user.is_admin or ( + self.membership is not None and self.membership.organisation.is_gouvernement + ) diff --git a/repondeur/zam_repondeur/visam/views/amendements_order.py b/repondeur/zam_repondeur/visam/views/amendements_order.py new file mode 100644 index 000000000..498209ae0 --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/amendements_order.py @@ -0,0 +1,25 @@ +from pyramid.request import Request +from pyramid.view import view_config + +from zam_repondeur.models import Amendement, DBSession +from zam_repondeur.resources import AmendementCollection + + +@view_config( + context=AmendementCollection, name="order", request_method="POST", renderer="json" +) +def reorder_amendements(context: AmendementCollection, request: Request) -> dict: + lecture = context.parent.model() + nums = request.json_body["order"] + amendements = [Amendement.get(lecture, num) for num in nums] + + # Reset positions first, so that we never have two with the same position + # (which would trigger an integrity error due to the unique constraint) + for amendement in amendements: + amendement.position = None + DBSession.flush() + + for i, amendement in enumerate(amendements, start=1): + amendement.position = i + + return {} diff --git a/repondeur/zam_repondeur/visam/views/article_preview.py b/repondeur/zam_repondeur/visam/views/article_preview.py new file mode 100644 index 000000000..24811e223 --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/article_preview.py @@ -0,0 +1,16 @@ +from pyramid.request import Request +from pyramid.view import view_config + +from zam_repondeur.resources import ArticleResource + + +@view_config( + context=ArticleResource, + name="preview", + request_method="GET", + renderer="article_preview.html", +) +def preview_article(context: ArticleResource, request: Request) -> dict: + return { + "article": context.model(), + } diff --git a/repondeur/zam_repondeur/visam/views/manage_members.py b/repondeur/zam_repondeur/visam/views/manage_members.py new file mode 100644 index 000000000..8c6f96a6f --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/manage_members.py @@ -0,0 +1,148 @@ +from datetime import date, datetime + +from pyramid.httpexceptions import HTTPFound +from pyramid.request import Request +from pyramid.response import Response +from pyramid.view import view_config, view_defaults + +from zam_repondeur.message import Message +from zam_repondeur.models import Chambre, DBSession, User +from zam_repondeur.visam.models import Organisation, UserMembership +from zam_repondeur.visam.models.events.membership import ( + MembershipAdded, + MembershipRemoved, +) +from zam_repondeur.visam.resources import MembersCollection + + +class MembersCollectionBase: + def __init__(self, context: MembersCollection, request: Request) -> None: + self.context = context + self.request = request + + +@view_defaults(context=MembersCollection, permission="manage") +class MembersList(MembersCollectionBase): + @view_config(request_method="GET", renderer="members_list.html") + def get(self) -> dict: + users = self.context.models() + last_event = self.context.events().first() + if last_event: + last_event_datetime = last_event.created_at + last_event_timestamp = ( + last_event_datetime - datetime(1970, 1, 1) + ).total_seconds() + else: + last_event_datetime = None + last_event_timestamp = None + organisations = DBSession.query(Organisation).all() + return { + "users": users, + "chambres": [Chambre.CCFP, Chambre.CSFPE], + "organisations": organisations, + "current_tab": "members", + "last_event_datetime": last_event_datetime, + "last_event_timestamp": last_event_timestamp, + } + + +@view_defaults(context=MembersCollection, permission="manage") +class MembersDelete(MembersCollectionBase): + @view_config(request_method="POST") + def post(self) -> Response: + user_pk = self.request.POST["user_pk"] + chambre_name = self.request.POST["chambre_name"] + organisation_name = self.request.POST["organisation_name"] + + user = DBSession.query(User).filter_by(pk=user_pk).first() + chambre = Chambre.from_string(chambre_name) + organisation = ( + DBSession.query(Organisation).filter_by(name=organisation_name).first() + ) + + membership = ( + DBSession.query(UserMembership) + .filter( + UserMembership.user_pk == user.pk, + UserMembership.chambre == chambre, + UserMembership.organisation == organisation, + ) + .first() + ) + MembershipRemoved.create(membership=membership, request=self.request) + self.request.session.flash( + Message( + cls="success", + text=( + f"Membre {user} - {organisation} retiré du " + f"{chambre.value} avec succès." + ), + ) + ) + return HTTPFound(location=self.request.resource_url(self.context)) + + +@view_defaults(context=MembersCollection, name="add", permission="manage") +class MembersAddForm(MembersCollectionBase): + @view_config(request_method="POST") + def post(self) -> Response: + user_pk = self.request.POST["user_pk"] + chambre_name = self.request.POST["chambre_name"] + organisation_name = self.request.POST["organisation_name"] + + user = DBSession.query(User).filter_by(pk=user_pk).first() + chambre = Chambre.from_string(chambre_name) + organisation = ( + DBSession.query(Organisation).filter_by(name=organisation_name).first() + ) + + membership = ( + DBSession.query(UserMembership) + .filter( + UserMembership.user_pk == user.pk, + UserMembership.chambre == chambre, + UserMembership.organisation == organisation, + ) + .first() + ) + if membership: + self.request.session.flash( + Message( + cls="warning", + text=( + f"Cet·te utilisateur·ice : {user} - {organisation} " + f"est déjà membre du {chambre.value}." + ), + ) + ) + return HTTPFound(location=self.request.resource_url(self.context)) + + MembershipAdded.create( + target_user=user, + target_chambre=chambre, + target_organisation=organisation, + comment=None, + request=self.request, + ) + + self.request.session.flash( + Message( + cls="success", + text=( + f"Membre {user} - {organisation} ajouté au " + f"{chambre.value} avec succès." + ), + ) + ) + return HTTPFound(location=self.request.resource_url(self.context)) + + +@view_config( + context=MembersCollection, + permission="manage", + name="journal", + renderer="members_journal.html", +) +def members_journal(context: MembersCollection, request: Request) -> Response: + events = context.events().all() + return {"events": events, "today": date.today(), "current_tab": "members"} diff --git a/repondeur/zam_repondeur/visam/views/seance_add_texte.py b/repondeur/zam_repondeur/visam/views/seance_add_texte.py new file mode 100644 index 000000000..58482fb83 --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/seance_add_texte.py @@ -0,0 +1,162 @@ +import logging +from collections import defaultdict +from datetime import date +from typing import Dict, List, Optional, Tuple + +from pyramid.httpexceptions import HTTPFound +from pyramid.response import Response +from pyramid.view import view_config, view_defaults +from selectolax.parser import HTMLParser +from sqlalchemy import func + +from zam_repondeur.message import Message +from zam_repondeur.models import ( + Article, + Chambre, + DBSession, + Dossier, + Lecture, + Phase, + Texte, + TypeTexte, + get_one_or_create, +) +from zam_repondeur.models.events.lecture import LectureCreee +from zam_repondeur.services.clean import clean_html +from zam_repondeur.slugs import slugify +from zam_repondeur.visam.models.seance import Formation, Seance +from zam_repondeur.visam.resources import SeanceResource +from zam_repondeur.visam.views.seance_item import SeanceViewBase + +logger = logging.getLogger(__name__) + + +@view_defaults(context=SeanceResource, name="add") +class SeanceAddTexteView(SeanceViewBase): + @view_config(request_method="GET", renderer="seance_add_texte.html") + def get(self) -> dict: + return { + "current_tab": "seances", + "seance_resource": self.context, + } + + @view_config(request_method="POST") + def post(self) -> Response: + titre = self.request.POST["titre"] + contenu = self.request.POST.get("contenu", "") + + # Un même texte, ou des versions successives d’un même texte, + # peut être à l’ordre du jour de plusieurs séances d’un conseil. + # Dans ce cas, les lectures seront regroupées au sein d’un même dossier. + dossier, created = self.find_or_create_dossier(titre, self.seance.lectures) + if created: + dossier.team = self.seance.team + + # Par contre, un même texte ne peut pas être examiné plusieurs + # fois lors d’une même séance d’un conseil... + lecture = self.find_lecture(dossier, self.seance) + if lecture is None: + texte = self.create_texte(self.seance.chambre) + + lecture = self.create_lecture(texte, dossier, self.seance.formation) + self.seance.lectures.append(lecture) + + self.create_articles(lecture, contenu) + + self.request.session.flash( + Message(cls="success", text="Texte créé avec succès.") + ) + else: + self.request.session.flash( + Message( + cls="warning", + text="Ce texte est déjà à l’ordre du jour de cette séance…", + ) + ) + + location = self.request.resource_url( + self.context["textes"][lecture.dossier.slug]["amendements"] + ) + return HTTPFound(location=location) + + def create_texte(self, chambre: Chambre) -> Texte: + max_numero = ( + DBSession.query(func.max(Texte.numero)) + .filter(Texte.type_ == TypeTexte.PROJET, Texte.chambre == chambre) + .scalar() + ) or 0 + texte = Texte.create( + type_=TypeTexte.PROJET, + chambre=chambre, + numero=max_numero + 1, + date_depot=date.today(), + ) + return texte + + def find_or_create_dossier( + self, titre: str, lectures: List[Lecture] + ) -> Tuple[Dossier, bool]: + slug = slugify(titre) + dossier, created = get_one_or_create( + Dossier, + slug=slug, + create_kwargs={"titre": titre, "an_id": "dummy-" + slug}, + ) + return dossier, created + + def find_lecture(self, dossier: Dossier, seance: Seance) -> Optional[Lecture]: + for lecture in seance.lectures: + if lecture.dossier is dossier: + return lecture + return None + + def create_lecture( + self, texte: Texte, dossier: Dossier, formation: Formation + ) -> Lecture: + organe = formation.value + lecture = Lecture.create( + dossier=dossier, + texte=texte, + phase=Phase.PREMIERE_LECTURE, + organe=organe, + titre=f"Première lecture – {organe}", + ) + LectureCreee.create(lecture=lecture, user=None) + return lecture + + def create_articles(self, lecture: Lecture, contenu: str) -> None: + for num, alineas in self.extract_articles(contenu).items(): + Article.create( + type="article", + num=num, + mult="", + pos="", + lecture=lecture, + content={ + str(i).zfill(3): alinea for i, alinea in enumerate(alineas, start=1) + }, + ) + + def extract_articles(self, contenu: str) -> Dict[str, List[str]]: + """ + Basic extraction of article contents + + Based on the following heuristics: + - ignore everything before the first article + - start a new article when a top-level element starts with "Article ..." + - stop when a top-level element starts with "Fait le ..." + """ + html = HTMLParser(clean_html(contenu)) + articles: Dict[str, List[str]] = defaultdict(list) + current_article = None + for node in html.css("body > *"): + texte = node.text() + if not texte.strip(): # skip empty lines + continue + if texte.startswith("Article "): + current_article = texte[len("Article ") :] + elif texte.startswith("Fait le "): # beginning of boilerplate + break + elif current_article is not None: + articles[current_article].append(node.html) + return articles diff --git a/repondeur/zam_repondeur/visam/views/seance_item.py b/repondeur/zam_repondeur/visam/views/seance_item.py new file mode 100644 index 000000000..49f2f8296 --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/seance_item.py @@ -0,0 +1,23 @@ +from pyramid.request import Request +from pyramid.view import view_config, view_defaults + +from zam_repondeur.visam.resources import SeanceResource + + +class SeanceViewBase: + def __init__(self, context: SeanceResource, request: Request) -> None: + self.context = context + self.request = request + self.seance = self.context.model() + + +@view_defaults(context=SeanceResource) +class SeanceView(SeanceViewBase): + @view_config(request_method="GET", renderer="seance_item.html") + def get(self) -> dict: + return { + "seance": self.seance, + "lectures": self.seance.lectures, + "current_tab": "seances", + "seance_resource": self.context, + } diff --git a/repondeur/zam_repondeur/visam/views/seance_reorder_textes.py b/repondeur/zam_repondeur/visam/views/seance_reorder_textes.py new file mode 100644 index 000000000..28a629d50 --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/seance_reorder_textes.py @@ -0,0 +1,17 @@ +from pyramid.view import view_config, view_defaults + +from zam_repondeur.models import Lecture +from zam_repondeur.visam.resources import SeanceResource +from zam_repondeur.visam.views.seance_item import SeanceViewBase + + +@view_defaults(context=SeanceResource, name="order") +class SeanceReorderTextesView(SeanceViewBase): + @view_config(request_method="POST", renderer="json") + def post(self) -> dict: + ordered_lecture_pks = (int(pk) for pk in self.request.json_body["order"]) + ordered_lectures = (Lecture.get_by_pk(pk) for pk in ordered_lecture_pks) + self.seance.lectures = [ + lecture for lecture in ordered_lectures if lecture is not None + ] + return {} diff --git a/repondeur/zam_repondeur/visam/views/seances_add.py b/repondeur/zam_repondeur/visam/views/seances_add.py new file mode 100644 index 000000000..037e26e04 --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/seances_add.py @@ -0,0 +1,59 @@ +import logging + +from pyramid.httpexceptions import HTTPBadRequest, HTTPFound +from pyramid.response import Response +from pyramid.view import view_config, view_defaults + +from zam_repondeur.message import Message +from zam_repondeur.models import Chambre, get_one_or_create +from zam_repondeur.services.fetch.dates import parse_date +from zam_repondeur.visam.models import Formation, Seance +from zam_repondeur.visam.resources import SeanceCollection +from zam_repondeur.visam.views.seances_list import SeanceCollectionBase + +logger = logging.getLogger(__name__) + + +@view_defaults(context=SeanceCollection, name="add", permission="create_seance") +class AddSeanceView(SeanceCollectionBase): + @view_config(request_method="GET", renderer="seances_add.html") + def get(self) -> dict: + return { + "current_tab": "seances", + "chambres": [ + (choice.name, f"{choice.value} ({choice.name})") + for choice in Chambre.__members__.values() + if choice.name not in {"AN", "SENAT"} + ], + "formations": [ + (choice.name, choice.value) for choice in Formation.__members__.values() + ], + } + + @view_config(request_method="POST") + def post(self) -> Response: + chambre = Chambre.__members__[self.request.POST["chambre"]] + date = parse_date(self.request.POST["date"]) + formation = Formation.__members__[self.request.POST["formation"]] + urgence_declaree = bool(int(self.request.POST["urgence_declaree"])) + + if date is None: + raise HTTPBadRequest("Date invalide") # TODO: better validation + + seance, created = get_one_or_create( + Seance, + chambre=chambre, + date=date, + formation=formation, + urgence_declaree=urgence_declaree, + ) + + if created: + self.request.session.flash( + Message(cls="success", text="Séance créée avec succès.") + ) + else: + self.request.session.flash( + Message(cls="warning", text="Cette séance existe déjà…") + ) + return HTTPFound(location=self.request.resource_url(self.context, seance.slug)) diff --git a/repondeur/zam_repondeur/visam/views/seances_list.py b/repondeur/zam_repondeur/visam/views/seances_list.py new file mode 100644 index 000000000..b39f19595 --- /dev/null +++ b/repondeur/zam_repondeur/visam/views/seances_list.py @@ -0,0 +1,32 @@ +import logging + +from pyramid.request import Request +from pyramid.view import view_config, view_defaults + +from zam_repondeur.visam.resources import SeanceCollection + +logger = logging.getLogger(__name__) + + +class SeanceCollectionBase: + def __init__(self, context: SeanceCollection, request: Request) -> None: + self.context = context + self.request = request + + +@view_defaults(context=SeanceCollection) +class ListSeancesView(SeanceCollectionBase): + @view_config(request_method="GET", renderer="seances_list.html") + def get(self) -> dict: + kwargs = {} + if not self.request.user.is_admin: + kwargs["chambres"] = self.request.user.chambres + seances = self.context.models(**kwargs) + + can_create_seance = self.request.has_permission("create_seance", self.context) + + return { + "seances": seances, + "can_create_seance": can_create_seance, + "current_tab": "seances", + }