diff --git a/.gitignore b/.gitignore index 311987ced..725eeece0 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ labonneboite/web/static/gen/ # virtualenv venv/ logs/ +__pycache__ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3d8551930..8dd6ba4f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,7 @@ services: restart: always build: context: . + dockerfile: docker/v3.6/Dockerfile args: options: --reload environment: diff --git a/docker/v3.10/Dockerfile b/docker/v3.10/Dockerfile new file mode 100644 index 000000000..f635d3aac --- /dev/null +++ b/docker/v3.10/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.10.7-slim-bullseye + + +# Set timezone +ENV TZ=Europe/Paris +ENV FLASK_APP web.app + +# for mysql support & git & french langage support +RUN apt update && apt install -y \ + git \ + python3-dev \ + default-libmysqlclient-dev \ + build-essential \ + locales \ + --no-install-recommends + +# switch working directory +WORKDIR /app +RUN mkdir -p /app/logs + +# Installing requirements +# COPY docker/v3.6/requirements.txt /requirements.txt +# RUN pip install -r /requirements.txt + +# File imports : source code +COPY docker/v3.10/ /app/ +COPY ./labonneboite /app/labonneboite +RUN pip install -e . +WORKDIR /app/labonneboite + +# unsupported local error : https://stackoverflow.com/questions/54802935/docker-unsupported-locale-setting-when-running-python-container +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ + && sed -i -e 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen \ + && locale-gen + +# building flask assets +RUN flask assets build + +# add the entrypoint +RUN chmod +x ./run.sh +RUN pip freeze +ENTRYPOINT ["/bin/bash", "./run.sh"] diff --git a/docker/v3.10/requirements.txt b/docker/v3.10/requirements.txt new file mode 100644 index 000000000..2dec63068 --- /dev/null +++ b/docker/v3.10/requirements.txt @@ -0,0 +1,172 @@ +alembic==1.8.1 +arabic-reshaper==2.1.3 +asn1crypto==1.5.1 +astroid==2.12.10 +asttokens==2.0.8 +async-generator==1.10 +attrs==22.1.0 +Babel==2.10.3 +backcall==0.2.0 +blinker==1.5 +build==0.8.0 +cachetools==5.2.0 +certifi==2022.9.14 +cffi==1.15.1 +chardet==5.0.0 +charset-normalizer==2.1.1 +click==8.1.3 +cryptography==38.0.1 +cssmin==0.2.0 +cssselect2==0.7.0 +decorator==5.1.1 +defusedxml==0.7.1 +dill==0.3.5.1 +dnspython==2.2.1 +EasyProcess==1.1 +ecdsa==0.18.0 +elastic-transport==8.4.0 +elasticsearch==8.4.1 +email-validator==1.3.0 +executing==1.0.0 +first==2.0.2 +Flask==2.2.2 +Flask-Admin==1.6.0 +Flask-Assets==2.0 +Flask-BabelEx==0.9.4 +Flask-Cors==3.0.10 +Flask-DebugToolbar==0.13.1 +Flask-Login==0.6.2 +Flask-Script==2.0.6 +Flask-Testing==0.8.1 +Flask-WTF==1.0.1 +future==0.18.2 +geographiclib==1.52 +geopy==2.2.0 +gevent==21.12.0 +google-api-core==2.10.1 +google-api-python-client==2.62.0 +google-auth==2.11.1 +google-auth-httplib2==0.1.0 +google-auth-oauthlib==0.5.3 +googleapis-common-protos==1.56.4 +greenlet==1.1.3 +gunicorn==20.1.0 +h11==0.13.0 +html5lib==1.1 +httplib2==0.20.4 +idna==3.4 +importlib-metadata==4.12.0 +ipdb==0.13.9 +ipython==8.5.0 +ipython-genutils==0.2.0 +isort==5.10.1 +itsdangerous==2.1.2 +jedi==0.18.1 +Jinja2==3.1.2 +jsmin==3.0.1 +# Editable install with no version control (LaBonneBoite==0.2) +-e /app +labonneboite-common @ git+https://github.com/StartupsPoleEmploi/labonneboite-common.git@f0dadcbb79338522169e586a9f4ec0a750920d00 +lazy-object-proxy==1.7.1 +line-profiler==3.5.1 +lxml==4.9.1 +mailjet-rest==1.3.4 +Mako==1.2.2 +MarkupSafe==2.1.1 +matplotlib-inline==0.1.6 +mccabe==0.7.0 +msgpack-python==0.5.6 +mysqlclient==2.1.1 +nose==1.3.7 +numpy==1.23.3 +oauthlib==3.2.1 +oscrypto==1.3.0 +outcome==1.2.0 +packaging==21.3 +pandas==1.5.0 +parameterized==0.8.1 +parso==0.8.3 +pep517==0.13.0 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.2.0 +pip-tools==6.8.0 +platformdirs==2.5.2 +prompt-toolkit==3.0.31 +protobuf==4.21.6 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycparser==2.21 +pycryptodomex==3.15.0 +Pygments==2.13.0 +pyHanko==0.14.0 +pyhanko-certvalidator==0.19.5 +pyjwkest==1.4.2 +PyJWT==2.5.0 +pylint==2.15.3 +pyparsing==3.0.9 +PyPDF2==2.10.9 +PyPDF3==1.0.6 +pyprof2calltree==1.4.5 +PySocks==1.7.1 +python-bidi==0.4.2 +python-dateutil==2.8.2 +python-editor==1.0.4 +python-jose==3.3.0 +python-slugify==6.1.2 +python3-openid==3.2.0 +pytz==2022.2.1 +pytz-deprecation-shim==0.1.0.post0 +PyVirtualDisplay==3.0 +PyYAML==6.0 +qrcode==7.3.1 +raven==6.10.0 +remote-pdb==2.1.0 +reportlab==3.6.11 +requests==2.28.1 +requests-oauthlib==1.3.1 +rsa==4.9 +selenium==4.4.3 +sentry-sdk==1.9.8 +six==1.16.0 +sniffio==1.3.0 +social-auth-app-flask==1.0.0 +social-auth-app-flask-sqlalchemy==1.0.1 +social-auth-core==4.3.0 +social-auth-storage-sqlalchemy==1.1.0 +sortedcontainers==2.4.0 +speaklater==1.3 +SQLAlchemy==1.3.24 +SQLAlchemy-Utils==0.38.3 +stack-data==0.5.0 +svglib==1.4.1 +text-unidecode==1.3 +tinycss2==1.1.1 +toml==0.10.2 +tomli==2.0.1 +tomlkit==0.11.4 +tqdm==4.64.1 +traitlets==5.4.0 +trio==0.21.0 +trio-websocket==0.9.2 +typing_extensions==4.3.0 +tzdata==2022.2 +tzlocal==4.2 +Unidecode==0.4.21 +uritemplate==4.1.1 +uritools==4.0.0 +urllib3==1.26.12 +validators==0.20.0 +wcwidth==0.2.5 +webassets==2.0 +webencodings==0.5.1 +Werkzeug==2.2.2 +wrapt==1.14.1 +wsproto==1.2.0 +WTForms==3.0.1 +xhtml2pdf==0.2.8 +zipp==3.8.1 +zope.event==4.5.0 +zope.interface==5.4.0 \ No newline at end of file diff --git a/docker/v3.10/setup.cfg b/docker/v3.10/setup.cfg new file mode 100644 index 000000000..2ed843971 --- /dev/null +++ b/docker/v3.10/setup.cfg @@ -0,0 +1,94 @@ +################################################## +# coverage # +################################################## +[coverage:run] +branch = True +parallel = true +concurrency=multiprocessing + +[coverage:report] +precision = 1 +show_missing = True +ignore_errors = True +exclude_lines = + pragma: no cover + raise NotImplementedError + def __repr__ + if settings.DEBUG + if __name__ == .__main__.: + if TYPE_CHECKING: +omit = + */test* + */migrations/* + manage.py + venv/* + +################################################## +# flake8 # +################################################## +[flake8] +ignore = + W503, # line break before binary operator + D100, # Missing docstring in public module + D101, # Missing docstring in public class + D102, # Missing docstring in public method + D104, # Missing docstring in public package + D106, # Missing docstring in public nested class + D200, # One-line docstring should fit on one line with quotes + D202, # No blank lines allowed after function docstring + D204, # 1 blank line required after class docstring + D205, # 1 blank line required between summary line and description + D400, # First line should end with a period + D406 # 1 blank line required before class docstring + +max-line-length = 120 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +enable-extensions = I,A,G,D +application-import-names = api,config,post,model_utils,saas,appointment,user +import-order-style = google +docstring-convention = numpy +exclude = migrations + +################################################## +# isort # +################################################## +[isort] +multi_line_output = 2 +line_length = 120 +order_by_type = false +force_to_top = labonneboite.conf + +################################################## +# mypy # +################################################## +[mypy] +ignore_missing_imports = false +follow_imports = silent +# no_strict_optional = true +show_error_codes = true +plugins = sqlmypy +disable_error_code = no-untyped-call +strict = true + +[mypy-alembic.*,astroid.*,flask_admin.*,dateutil.*,sqlalchemy_utils.*,babel.dates.*,easyprocess.*,elasticsearch.*,elasticsearch.exceptions.*,elasticsearch.helpers.*,flask_admin.*,flask_admin.contrib.sqla.*,flask_assets.*,flask_babelex.*,flask_debugtoolbar.*,flask.ext.cors.*,flask_login.*,flask_script.*,flask_testing.*,flask_wtf.*,flask_wtf.csrf.*,geopy.*,geopy.distance.*,googleapiclient.discovery.*,google_auth_oauthlib.flow.*,google.auth.transport.requests.*,ipdb.*,locust.*,mailjet_rest.*,MySQLdb.*,numpy.*,pandas.*,parameterized.*,pyprof2calltree.*,pyvirtualdisplay.*,sklearn.*,sklearn.metrics.*,social_core.*,social_core.backends.open_id_connect.*,social_core.exceptions.*,social_flask.routes.*,social_flask_sqlalchemy.models.*,social_flask.utils.*,unidecode.*,validators.*,wtforms.*,wtforms.fields.html5.*,wtforms.validators.*,wtforms.widgets.*,xhtml2pdf.*] +ignore_missing_imports = true + +################################################## +# yapf # +################################################## +[yapf] +based_on_style = google +column_limit = 120 +split_before_logical_operator = true +split_before_dot = true +coalesce_brackets = true +align_closing_bracket_with_visual_indent = true +allow_split_before_dict_value = false +blank_line_before_nested_class_or_def = true +blank_lines_around_top_level_definition = 2 + +[tool:pytest] +python_files = test_*.py +env = + LBB_ENV=test \ No newline at end of file diff --git a/docker/v3.10/setup.py b/docker/v3.10/setup.py new file mode 100644 index 000000000..022feb54a --- /dev/null +++ b/docker/v3.10/setup.py @@ -0,0 +1,47 @@ +import os +from setuptools import setup +import re + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +pip_editable_with_egg_regex = re.compile("-e (.+#egg=(.+))") + + +def requirement_to_install_require(requirement: str): + result = pip_editable_with_egg_regex.match(requirement) + if result: + return f"{result.group(2)} @ {result.group(1)}" + return requirement + + +install_requires = [ + requirement_to_install_require(req) for req in open("requirements.txt") +] + +setup( + name="LaBonneBoite", + version="0.2", + author="La Bonne Boite", + author_email="labonneboite@pole-emploi.fr", + description=(""), + packages=[ + "labonneboite", + ], + include_package_data=True, + install_requires=install_requires, + entry_points={ + "console_scripts": [ + "create_index = labonneboite.scripts.create_index:run", + ], + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + ], +) diff --git a/Dockerfile b/docker/v3.6/Dockerfile similarity index 100% rename from Dockerfile rename to docker/v3.6/Dockerfile diff --git a/labonneboite/web/admin/views/office_admin_add.py b/labonneboite/web/admin/views/office_admin_add.py index 1122da42d..9cdeade8b 100644 --- a/labonneboite/web/admin/views/office_admin_add.py +++ b/labonneboite/web/admin/views/office_admin_add.py @@ -10,8 +10,18 @@ from labonneboite.common.scoring import get_hirings_from_score from labonneboite.conf import settings from labonneboite.web.admin.forms import code_commune_validator, zip_code_validator -from labonneboite.web.admin.forms import nospace_filter, phone_validator, strip_filter, siret_validator, naf_validator -from labonneboite.web.admin.utils import datetime_format, AdminModelViewMixin, SelectForChoiceTypeField +from labonneboite.web.admin.forms import ( + nospace_filter, + phone_validator, + strip_filter, + siret_validator, + naf_validator, +) +from labonneboite.web.admin.utils import ( + datetime_format, + AdminModelViewMixin, + SelectForChoiceTypeField, +) from labonneboite.scripts import create_index @@ -24,187 +34,190 @@ class OfficeAdminAddModelView(AdminModelViewMixin, ModelView): # type: ignore can_delete = False can_edit = False can_view_details = True - column_searchable_list = ['siret', 'company_name', 'office_name'] - column_default_sort = ('date_created', True) + column_searchable_list = ["siret", "company_name", "office_name"] + column_default_sort = ("date_created", True) page_size = 100 column_list = [ - 'siret', - 'company_name', - 'reason', - 'date_created', - 'date_updated', + "siret", + "company_name", + "reason", + "date_created", + "date_updated", ] column_details_list = [ - 'siret', - 'company_name', - 'office_name', - 'naf', - 'street_number', - 'street_name', - 'zipcode', - 'city_code', - 'email', - 'tel', - 'website', - 'flag_alternance', - 'flag_junior', - 'flag_senior', - 'flag_handicap', - 'departement', - 'headcount', - 'hiring', - 'score_alternance', - 'x', - 'y', - 'reason', - 'created_by', - 'date_created', - 'updated_by', - 'date_updated', + "siret", + "company_name", + "office_name", + "naf", + "street_number", + "street_name", + "zipcode", + "city_code", + "email", + "tel", + "website", + "flag_alternance", + "flag_junior", + "flag_senior", + "flag_handicap", + "departement", + "headcount", + "hiring", + "score_alternance", + "x", + "y", + "reason", + "created_by", + "date_created", + "updated_by", + "date_updated", ] column_formatters = { - 'date_created': datetime_format, - 'date_updated': datetime_format, + "date_created": datetime_format, + "date_updated": datetime_format, } column_labels = { - 'siret': "Siret", - 'company_name': "Raison sociale", - 'office_name': "Enseigne", - 'naf': "Code NAF", - 'street_number': "Numero rue", - 'street_name': "Libellé rue", - 'zipcode': "Code postal", - 'city_code': "Code commune", - 'email': "Email", - 'tel': "Téléphone", - 'website': "Site web", - 'flag_alternance': "Drapeau alternance", - 'flag_junior': "Drapeau junior", - 'flag_senior': "Drapeau senior", - 'flag_handicap': "Drapeau handicap", - 'departement': "Département", - 'headcount': "Tranche effectif", - 'hiring': "hiring", - 'score_alternance': "Score alternance", - 'x': "Longitude", - 'y': "Latitude", - 'reason': "Raison", - 'date_created': "Date de création", - 'date_updated': "Date de modification", - 'created_by': "Créé par", - 'updated_by': "Modifié par", + "siret": "Siret", + "company_name": "Raison sociale", + "office_name": "Enseigne", + "naf": "Code NAF", + "street_number": "Numero rue", + "street_name": "Libellé rue", + "zipcode": "Code postal", + "city_code": "Code commune", + "email": "Email", + "tel": "Téléphone", + "website": "Site web", + "flag_alternance": "Drapeau alternance", + "flag_junior": "Drapeau junior", + "flag_senior": "Drapeau senior", + "flag_handicap": "Drapeau handicap", + "departement": "Département", + "headcount": "Tranche effectif", + "hiring": "hiring", + "score_alternance": "Score alternance", + "x": "Longitude", + "y": "Latitude", + "reason": "Raison", + "date_created": "Date de création", + "date_updated": "Date de modification", + "created_by": "Créé par", + "updated_by": "Modifié par", } column_descriptions = { - 'reason': "Raison de l'ajout.", - 'hiring': "Valeur recommandée : entre " - f"{scoring.get_hirings_from_score(80)} et {scoring.get_hirings_from_score(90)}", - 'score_alternance': "Valeur recommandée : entre 80 et 90", + "reason": "Raison de l'ajout.", + "hiring": "Valeur recommandée : entre " + f"{scoring.get_hirings_from_score(80)} et {scoring.get_hirings_from_score(90)}", + "score_alternance": "Valeur recommandée : entre 80 et 90", } form_columns = [ - 'siret', - 'company_name', - 'office_name', - 'naf', - 'street_number', - 'street_name', - 'zipcode', - 'city_code', - 'departement', - 'email', - 'tel', - 'website', - 'flag_alternance', - 'flag_junior', - 'flag_senior', - 'flag_handicap', - 'headcount', - 'hiring', - 'score_alternance', - 'y', - 'x', - 'reason', + "siret", + "company_name", + "office_name", + "naf", + "street_number", + "street_name", + "zipcode", + "city_code", + "departement", + "email", + "tel", + "website", + "flag_alternance", + "flag_junior", + "flag_senior", + "flag_handicap", + "headcount", + "hiring", + "score_alternance", + "y", + "x", + "reason", ] form_overrides = { - 'headcount': SelectForChoiceTypeField, + "headcount": SelectForChoiceTypeField, } form_args = { - 'siret': { - 'filters': [strip_filter, nospace_filter], - 'validators': [DataRequired(), siret_validator], + "siret": { + "filters": [strip_filter, nospace_filter], + "validators": [DataRequired(), siret_validator], }, - - 'company_name': { - 'filters': [strip_filter], + "company_name": { + "filters": [strip_filter], }, - 'office_name': { - 'filters': [strip_filter], + "office_name": { + "filters": [strip_filter], }, - 'naf': { - 'filters': [strip_filter, nospace_filter], - 'validators': [naf_validator] + "naf": { + "filters": [strip_filter, nospace_filter], + "validators": [naf_validator], }, - 'street_number': { - 'filters': [strip_filter, nospace_filter], + "street_number": { + "filters": [strip_filter, nospace_filter], }, - 'street_name': { - 'filters': [strip_filter], + "street_name": { + "filters": [strip_filter], }, - 'zipcode': { - 'filters': [strip_filter, nospace_filter], - 'validators': [zip_code_validator], + "zipcode": { + "filters": [strip_filter, nospace_filter], + "validators": [zip_code_validator], }, - 'city_code': { - 'filters': [strip_filter, nospace_filter], - 'validators': [code_commune_validator], + "city_code": { + "filters": [strip_filter, nospace_filter], + "validators": [code_commune_validator], }, - 'departement': { - 'filters': [strip_filter, nospace_filter], + "departement": { + "filters": [strip_filter, nospace_filter], }, - 'email': { - 'validators': [validators.optional(), validators.Email()], + "email": { + "validators": [validators.optional(), validators.Email()], }, - 'tel': { - 'filters': [strip_filter, nospace_filter], - 'validators': [validators.optional(), phone_validator], + "tel": { + "filters": [strip_filter, nospace_filter], + "validators": [validators.optional(), phone_validator], }, - 'website': { - 'filters': [strip_filter], - 'validators': [validators.optional(), validators.URL()], + "website": { + "filters": [strip_filter], + "validators": [validators.optional(), validators.URL()], }, - 'headcount': { - 'choices': settings.HEADCOUNT_INSEE_CHOICES, + "headcount": { + "choices": settings.HEADCOUNT_INSEE_CHOICES, }, - 'hiring': { - 'validators': [validators.NumberRange(min=0, max=get_hirings_from_score(100))], + "hiring": { + "validators": [ + validators.NumberRange(min=0, max=get_hirings_from_score(100)) + ], }, - 'score_alternance': { - 'validators': [validators.NumberRange(min=0, max=100)], + "score_alternance": { + "validators": [validators.NumberRange(min=0, max=100)], }, - 'reason': { - 'filters': [strip_filter], + "reason": { + "filters": [strip_filter], }, - 'x': { - 'filters': [strip_filter, nospace_filter], - 'validators': [validators.required()], + "x": { + "filters": [strip_filter, nospace_filter], + "validators": [DataRequired()], # sytt: instead of [validators.required()] }, - 'y': { - 'filters': [strip_filter, nospace_filter], - 'validators': [validators.required()], + "y": { + "filters": [strip_filter, nospace_filter], + "validators": [DataRequired()], # sytt: instead of [validators.required()] }, } - def after_model_change(self, form: BaseForm, model: OfficeAdminAdd, is_created: bool) -> None: + def after_model_change( + self, form: BaseForm, model: OfficeAdminAdd, is_created: bool + ) -> None: """ Add new office in ElacticSearch and MySQL DB and remove it from OfficeAdminRemove if it exists in such DB """ - OfficeAdminRemove.query.filter_by(siret=form.data['siret']).delete() + OfficeAdminRemove.query.filter_by(siret=form.data["siret"]).delete() create_index.add_individual_office(model) diff --git a/labonneboite/web/contact_form/forms.py b/labonneboite/web/contact_form/forms.py index f3f42825d..9ac1d38a7 100644 --- a/labonneboite/web/contact_form/forms.py +++ b/labonneboite/web/contact_form/forms.py @@ -1,8 +1,17 @@ from flask import request from flask_wtf import FlaskForm -from wtforms import BooleanField, HiddenField, RadioField, SelectMultipleField, StringField, TextAreaField -from wtforms import validators +from wtforms import ( + BooleanField, + HiddenField, + RadioField, + SelectMultipleField, + StringField, + TextAreaField, +) +from wtforms import validators # sytt : this is import if it works, it's a fluke... + +# from wtforms.fields import EmailField, TelField # compatibility 3.10 : wtfform > 3.0.0 from wtforms.fields.html5 import EmailField, TelField from wtforms.validators import DataRequired, Email, Optional, Regexp, URL from wtforms.widgets import ListWidget, CheckboxInput @@ -12,7 +21,7 @@ PHONE_REGEX = r"^(0|\+33)[1-9]([-. ]?[0-9]{2}){4}$" -SIRET_REGEX = r'[0-9]{14}' +SIRET_REGEX = r"[0-9]{14}" class MultiCheckboxField(SelectMultipleField): @@ -24,6 +33,7 @@ class MultiCheckboxField(SelectMultipleField): https://wtforms.readthedocs.io/en/stable/specific_problems.html#specialty-field-tricks """ + widget = ListWidget(prefix_label=False) option_widget = CheckboxInput() @@ -31,53 +41,55 @@ class MultiCheckboxField(SelectMultipleField): class OfficeIdentificationForm(FlaskForm): siret = StringField( - 'N° de Siret *', + "N° de Siret *", validators=[ DataRequired(), - Regexp(SIRET_REGEX, message=("Le siret de l'établissement est invalide (14 chiffres)")) + Regexp( + SIRET_REGEX, + message=("Le siret de l'établissement est invalide (14 chiffres)"), + ), ], - description= - "14 chiffres, sans espace. Exemple: 36252187900034
" - 'Retrouver mon numéro de siret ', + description="14 chiffres, sans espace. Exemple: 36252187900034
" + 'Retrouver mon numéro de siret ', ) - last_name = StringField('Nom *', validators=[DataRequired()]) - first_name = StringField('Prénom *', validators=[DataRequired()]) + last_name = StringField("Nom *", validators=[DataRequired()]) + first_name = StringField("Prénom *", validators=[DataRequired()]) phone = TelField( - 'Téléphone *', + "Téléphone *", validators=[DataRequired(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) email = EmailField( - 'Adresse email *', + "Adresse email *", validators=[DataRequired(), Email()], - render_kw={"placeholder": "exemple@domaine.com"} + render_kw={"placeholder": "exemple@domaine.com"}, ) class OfficeHiddenIdentificationForm(FlaskForm): - siret = HiddenField('Siret *', validators=[DataRequired()]) - last_name = HiddenField('Nom *', validators=[DataRequired()]) - first_name = HiddenField('Prénom *', validators=[DataRequired()]) + siret = HiddenField("Siret *", validators=[DataRequired()]) + last_name = HiddenField("Nom *", validators=[DataRequired()]) + first_name = HiddenField("Prénom *", validators=[DataRequired()]) phone = HiddenField( - 'Téléphone *', + "Téléphone *", validators=[DataRequired(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) email = HiddenField( - 'Adresse email *', + "Adresse email *", validators=[DataRequired(), Email()], - render_kw={"placeholder": "exemple@domaine.com"} + render_kw={"placeholder": "exemple@domaine.com"}, ) class OfficeOtherRequestForm(OfficeHiddenIdentificationForm): comment = TextAreaField( - 'Votre message*', + "Votre message*", validators=[DataRequired(), validators.length(max=15000)], - description="15000 caractères maximum" + description="15000 caractères maximum", ) @@ -103,8 +115,10 @@ def __init__(self, *args, **kwargs): associated with the current office. """ super().__init__(*args, **kwargs) - self.office = kwargs.pop('office') - self.romes_choices = [(rome.code, rome.name) for rome in self.office.romes_for_naf_mapping] + self.office = kwargs.pop("office") + self.romes_choices = [ + (rome.code, rome.name) for rome in self.office.romes_for_naf_mapping + ] self.romes_to_keep.choices = self.romes_choices self.romes_alternance_to_keep.choices = self.romes_choices @@ -123,28 +137,34 @@ def validate(self): # `extra_romes_to_add` and `extra_romes_alternance_to_add` are pupulated via JavaScript. # Those fields are defined outside of the form class so we use `request.form` to get them. extra_romes_to_add = [ - rome for rome in request.form.getlist('extra_romes_to_add') - if rome in settings.ROME_DESCRIPTIONS and rome not in self.office.romes_codes + rome + for rome in request.form.getlist("extra_romes_to_add") + if rome in settings.ROME_DESCRIPTIONS + and rome not in self.office.romes_codes ] extra_romes_alternance_to_add = [ - rome for rome in request.form.getlist('extra_romes_alternance_to_add') - if rome in settings.ROME_DESCRIPTIONS and rome not in self.office.romes_codes + rome + for rome in request.form.getlist("extra_romes_alternance_to_add") + if rome in settings.ROME_DESCRIPTIONS + and rome not in self.office.romes_codes ] # Checked ROME codes. romes_to_keep = self.romes_to_keep.data or [] romes_to_add = set(romes_to_keep + extra_romes_to_add) romes_alternance_to_keep = self.romes_alternance_to_keep.data or [] - romes_alternance_to_add = set(romes_alternance_to_keep + extra_romes_alternance_to_add) + romes_alternance_to_add = set( + romes_alternance_to_keep + extra_romes_alternance_to_add + ) # Unchecked ROME codes. romes_to_remove = self.office.romes_codes - romes_to_add romes_alternance_to_remove = self.office.romes_codes - romes_alternance_to_add - setattr(self, 'romes_to_add', romes_to_add) - setattr(self, 'romes_alternance_to_add', romes_alternance_to_add) - setattr(self, 'romes_to_remove', romes_to_remove) - setattr(self, 'romes_alternance_to_remove', romes_alternance_to_remove) + setattr(self, "romes_to_add", romes_to_add) + setattr(self, "romes_alternance_to_add", romes_alternance_to_add) + setattr(self, "romes_to_remove", romes_to_remove) + setattr(self, "romes_alternance_to_remove", romes_alternance_to_remove) return True @@ -152,57 +172,57 @@ def validate(self): class OfficeUpdateCoordinatesForm(OfficeHiddenIdentificationForm): CONTACT_MODES = ( - ('mail', 'Par courrier'), - ('email', 'Par email'), - ('phone', 'Par téléphone'), - ('office', 'Sur place'), - ('website', 'Via votre site internet'), + ("mail", "Par courrier"), + ("email", "Par email"), + ("phone", "Par téléphone"), + ("office", "Sur place"), + ("website", "Via votre site internet"), ) CONTACT_MODES_LABELS = dict(CONTACT_MODES) # Note : we add new_ to avoid conflict with request.args new_contact_mode = RadioField( - 'Mode de contact à privilégier', - choices=CONTACT_MODES, - default='email' + "Mode de contact à privilégier", choices=CONTACT_MODES, default="email" ) new_website = StringField( - 'Site Internet', - validators=[URL(), Optional()], render_kw={"placeholder": "http://exemple.com"} + "Site Internet", + validators=[URL(), Optional()], + render_kw={"placeholder": "http://exemple.com"}, ) new_email = EmailField( - 'Email recruteur', - validators=[Email(), Optional()], render_kw={"placeholder": "exemple@domaine.com"} + "Email recruteur", + validators=[Email(), Optional()], + render_kw={"placeholder": "exemple@domaine.com"}, ) new_phone = StringField( - 'Téléphone', + "Téléphone", validators=[Optional(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) - social_network = StringField('Réseau social', validators=[URL(), Optional()]) + social_network = StringField("Réseau social", validators=[URL(), Optional()]) new_email_alternance = EmailField( - 'Email recruteur spécialisé alternance', + "Email recruteur spécialisé alternance", validators=[validators.optional(), Email()], - render_kw={"placeholder": "exemple-alternance@domaine.com"} + render_kw={"placeholder": "exemple-alternance@domaine.com"}, ) new_phone_alternance = StringField( - 'Téléphone du recruteur spécialisé alternance', + "Téléphone du recruteur spécialisé alternance", validators=[validators.optional(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) rgpd_consent = BooleanField( - 'En cochant cette case, vous consentez à diffuser des données à caractère personnel sur les services numériques de Pôle emploi.', - [validators.required()] + "En cochant cette case, vous consentez à diffuser des données à caractère personnel sur les services numériques de Pôle emploi.", + validators=[DataRequired()], # sytt: instead of [validators.required()] ) class OfficeRemoveForm(OfficeHiddenIdentificationForm): remove_lbb = BooleanField( - 'Supprimer mon entreprise du service La Bonne Boite puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site', - [validators.optional()] + "Supprimer mon entreprise du service La Bonne Boite puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site", + [validators.optional()], ) remove_lba = BooleanField( - 'Supprimer mon entreprise du service La Bonne Alternance puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site', - [validators.optional()] + "Supprimer mon entreprise du service La Bonne Alternance puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site", + [validators.optional()], )