├── VERSION
├── app
├── __init__.py
├── api
│ ├── __init__.py
│ └── route.py
├── groupe
│ ├── __init__.py
│ ├── forms.py
│ └── route.py
├── liste
│ ├── __init__.py
│ ├── forms.py
│ └── route.py
├── login
│ ├── __init__.py
│ └── route.py
├── t_roles
│ ├── __init__.py
│ ├── forms.py
│ └── route.py
├── utils
│ ├── __init__.py
│ ├── utils_all.py
│ ├── errors.py
│ └── utilssqlalchemy.py
├── migrations
│ ├── __init__.py
│ ├── versions
│ │ ├── __init__.py
│ │ ├── 6ec215fe023e_upgrade_utilisateurs_schema.py
│ │ ├── f63a8f44c969_usershub_samples.py
│ │ └── 9445a69f2bed_usershub.py
│ ├── README
│ ├── script.py.mako
│ ├── alembic.ini
│ └── env.py
├── static
│ ├── .nvmrc
│ ├── style.css
│ ├── images
│ │ └── favicon.png
│ ├── package.json
│ ├── package-lock.json
│ └── transfer.js
├── t_profils
│ ├── __init__.py
│ ├── forms.py
│ └── route.py
├── temp_users
│ ├── __init__.py
│ └── routes.py
├── bib_organismes
│ ├── __init__.py
│ ├── forms.py
│ └── route.py
├── t_applications
│ ├── __init__.py
│ └── forms.py
├── templates
│ ├── constants.js
│ ├── alert_messages.html
│ ├── group.html
│ ├── scripts.html
│ ├── list.html
│ ├── profil.html
│ ├── application_role_profil_form.html
│ ├── user_pass.html
│ ├── generic_table.html
│ ├── base.html
│ ├── organism.html
│ ├── user.html
│ ├── application.html
│ ├── temp_users.html
│ ├── login.html
│ ├── info_list.html
│ ├── head-appli.html
│ ├── librairies.html
│ ├── info_application.html
│ ├── info_organisme.html
│ ├── app_profils.html
│ ├── tobelong.html
│ ├── info_group.html
│ ├── table_database.html
│ ├── info_user.html
│ └── wtf_bootstrap_4.html
├── env.py
├── genericRepository.py
└── app.py
├── var
└── log
│ └── .gitkeep
├── .dockerignore
├── docs
├── requirements.readthedocs.txt
├── images
│ ├── capture-application.jpg
│ └── usershub-applications.jpg
├── conf.py
├── index.rst
├── auteurs.rst
├── migration-v1v2.rst
├── FAQ.rst
├── docker.rst
└── installation.rst
├── requirements-dependencies.in
├── config
├── .gitignore
├── docker_config.py
├── config.py.sample
└── settings.ini.sample
├── db
└── add-extensions.sql
├── requirements-dev.in
├── requirements.in
├── MANIFEST.in
├── requirements-submodules.in
├── tmpfiles-usershub.conf
├── .flaskenv
├── usershub_apache.conf
├── .gitmodules
├── requirements-common.in
├── log_rotate
├── .gitignore
├── .github
└── workflows
│ ├── lint.yml
│ ├── docker.yml
│ └── test_install.yml
├── Makefile
├── usershub.service
├── .readthedocs.yaml
├── setup.py
├── install_db.sh
├── docker-compose.yml
├── install_app.sh
├── README.rst
├── Dockerfile
├── requirements.txt
└── requirements-dev.txt
/VERSION:
--------------------------------------------------------------------------------
1 | 2.4.7
2 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/var/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/groupe/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/liste/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/login/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/t_roles/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/static/.nvmrc:
--------------------------------------------------------------------------------
1 | v10.15.3
--------------------------------------------------------------------------------
/app/t_profils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/temp_users/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/bib_organismes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/static/style.css:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/t_applications/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/migrations/versions/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | app/static/node_modules/
3 |
--------------------------------------------------------------------------------
/app/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/docs/requirements.readthedocs.txt:
--------------------------------------------------------------------------------
1 | sphinx-rtd-theme>=1.0.0
2 |
--------------------------------------------------------------------------------
/requirements-dependencies.in:
--------------------------------------------------------------------------------
1 | pypnusershub>=3.0.3,<4.0.0
2 |
--------------------------------------------------------------------------------
/app/templates/constants.js:
--------------------------------------------------------------------------------
1 | var url_app = "{{url_application}}";
2 |
--------------------------------------------------------------------------------
/config/.gitignore:
--------------------------------------------------------------------------------
1 | dbconnexions.json
2 | settings.ini
3 | config.php
4 |
--------------------------------------------------------------------------------
/db/add-extensions.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
2 |
--------------------------------------------------------------------------------
/requirements-dev.in:
--------------------------------------------------------------------------------
1 | -r requirements-common.in
2 | -r requirements-submodules.in
3 |
--------------------------------------------------------------------------------
/requirements.in:
--------------------------------------------------------------------------------
1 | -r requirements-common.in
2 | -r requirements-dependencies.in
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include VERSION
2 | include LICENSE
3 | include README.rst
4 | include requirements.in
5 |
--------------------------------------------------------------------------------
/requirements-submodules.in:
--------------------------------------------------------------------------------
1 | -e file:dependencies/UsersHub-authentification-module#egg=pypnusershub
2 |
--------------------------------------------------------------------------------
/app/static/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PnX-SI/UsersHub/HEAD/app/static/images/favicon.png
--------------------------------------------------------------------------------
/tmpfiles-usershub.conf:
--------------------------------------------------------------------------------
1 | d /run/usershb 0750 ${USER} ${USER} -
2 | d /var/log/usershub 0750 ${USER} ${USER} -
3 |
--------------------------------------------------------------------------------
/docs/images/capture-application.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PnX-SI/UsersHub/HEAD/docs/images/capture-application.jpg
--------------------------------------------------------------------------------
/docs/images/usershub-applications.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PnX-SI/UsersHub/HEAD/docs/images/usershub-applications.jpg
--------------------------------------------------------------------------------
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=app.app:create_app
2 | FLASK_SQLALCHEMY_DB = "app.env.db"
3 | FLASK_RUN_PORT=5001
4 | FLASK_ENV=development
5 | FLASK_DEBUG=1
6 |
--------------------------------------------------------------------------------
/usershub_apache.conf:
--------------------------------------------------------------------------------
1 | # Configuration UsersHub
2 |
3 | ProxyPass http://127.0.0.1:5001/usershub
4 | ProxyPassReverse http://127.0.0.1:5001/usershub
5 |
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "dependencies/UsersHub-authentification-module"]
2 | path = dependencies/UsersHub-authentification-module
3 | url = https://github.com/PnX-SI/UsersHub-authentification-module
4 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | import sphinx_rtd_theme # noqa
2 |
3 | extensions = [
4 | "sphinx_rtd_theme",
5 | ]
6 |
7 | project = "UsersHub"
8 | html_theme = "sphinx_rtd_theme"
9 | pygments_style = "sphinx"
10 |
--------------------------------------------------------------------------------
/config/docker_config.py:
--------------------------------------------------------------------------------
1 | PASS_METHOD = "hash"
2 | FILL_MD5_PASS = False
3 | COOKIE_EXPIRATION = 3600
4 | COOKIE_AUTORENEW = True
5 | ACTIVATE_API = True
6 | ACTIVATE_APP = True
7 | SQLALCHEMY_TRACK_MODIFICATIONS = False
8 |
--------------------------------------------------------------------------------
/app/env.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask_sqlalchemy import SQLAlchemy
3 |
4 |
5 | """
6 | Création de la base avec sqlalchemy
7 | """
8 |
9 | os.environ["FLASK_SQLALCHEMY_DB"] = "app.env.db"
10 | db = SQLAlchemy()
11 |
--------------------------------------------------------------------------------
/requirements-common.in:
--------------------------------------------------------------------------------
1 | Flask
2 | Flask-SQLAlchemy
3 | Flask-WTF
4 | Flask-Migrate
5 | psycopg2
6 | WTForms-Components
7 | WTForms
8 | python-dateutil
9 | python-dotenv
10 | marshmallow-sqlalchemy
11 | gunicorn
12 | email_validator
13 |
--------------------------------------------------------------------------------
/log_rotate:
--------------------------------------------------------------------------------
1 | /var/log/usershub/usershub.log {
2 | su ${USER} ${USER}
3 | daily
4 | rotate 8
5 | size 100M
6 | create
7 | compress
8 | postrotate
9 | systemctl reload usershub || true
10 | endscript
11 | }
12 |
--------------------------------------------------------------------------------
/app/utils/utils_all.py:
--------------------------------------------------------------------------------
1 | """
2 | General utils
3 | """
4 |
5 |
6 | def strigify_dict(my_dict):
7 | returned_string = ""
8 | for key, value in my_dict.items():
9 | returned_string += " - " + ", ".join(value)
10 | return returned_string
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.*~
2 | log/
3 |
4 | usershub2.pid
5 | config/config.py
6 | venv
7 | *.pyc
8 | __pycache__
9 | node_modules
10 | fontawesome*
11 |
12 | .vscode
13 |
14 | *.php
15 |
16 | app/templates/mails/sign_up_confirmation.html
17 |
18 | /docs/changelog.html
19 |
20 | *.swp
21 | *.swo
22 |
23 | *.egg-info/
24 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | backend:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | with:
11 | fetch-depth: 0
12 | - name: Backend code formatting check (Black)
13 | uses: psf/black@stable
14 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. image:: http://geonature.fr/img/logo-pne.jpg
2 | :target: http://www.ecrins-parcnational.fr
3 |
4 | =================================
5 | Bienvenue dans la doc de UsersHub
6 | =================================
7 |
8 | .. toctree::
9 | :maxdepth: 2
10 |
11 | installation
12 | migration-v1v2
13 | FAQ
14 | auteurs
15 | changelog
16 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | # Makefile for UsersHub
3 |
4 | init_config:
5 | cp config/config.py.sample config/config.py
6 | cp config/settings.ini.sample config/settings.ini
7 |
8 | run:
9 | . venv/bin/activate && flask run
10 |
11 | start: run
12 |
13 | install:
14 | ./install_app.sh
15 | ./install_db.sh
16 |
17 | autoupgrade:
18 | . venv/bin/activate && flask db upgrade usershub@head
19 | . venv/bin/activate && flask db upgrade utilisateurs@head
--------------------------------------------------------------------------------
/app/login/route.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, redirect, url_for, render_template, Blueprint, current_app
2 |
3 | from app.env import db
4 | from app.models import TApplications
5 |
6 | route = Blueprint("login", __name__)
7 |
8 |
9 | @route.route("login", methods=["GET"])
10 | def login():
11 | app = TApplications.query.filter_by(
12 | code_application=current_app.config["CODE_APPLICATION"]
13 | ).one()
14 | return render_template("login.html", id_app=app.id_application)
15 |
--------------------------------------------------------------------------------
/app/templates/alert_messages.html:
--------------------------------------------------------------------------------
1 | {% with messages = get_flashed_messages(with_categories=true) %}
2 | {% if messages %}
3 |
4 | {% for category, message in messages %}
5 | {% if category == 'error' %}
6 |
7 | {{message}}
8 |
9 | {% else %}
10 |
11 | {{message}}
12 |
13 | {% endif %}
14 | {% endfor %}
15 |
16 | {% endif %}
17 | {% endwith %}
--------------------------------------------------------------------------------
/app/migrations/versions/6ec215fe023e_upgrade_utilisateurs_schema.py:
--------------------------------------------------------------------------------
1 | """upgrade utilisateurs schema
2 |
3 | Revision ID: 6ec215fe023e
4 | Revises: 9445a69f2bed
5 | Create Date: 2021-09-30 16:29:25.531376
6 |
7 | """
8 |
9 | from alembic import op
10 | import sqlalchemy as sa
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = "6ec215fe023e"
15 | down_revision = "9445a69f2bed"
16 | branch_labels = None
17 | depends_on = ("951b8270a1cf",) # utilisateurs
18 |
19 |
20 | def upgrade():
21 | pass
22 |
23 |
24 | def downgrade():
25 | pass
26 |
--------------------------------------------------------------------------------
/docs/auteurs.rst:
--------------------------------------------------------------------------------
1 | =======
2 | AUTEURS
3 | =======
4 |
5 | Parc national des Ecrins
6 | ------------------------
7 |
8 | * Gil Deluermoz
9 | * Camille Monchicourt
10 | * Gabin Laumond
11 |
12 | .. image:: http://geonature.fr/img/logo-pne.jpg
13 | :target: http://www.ecrins-parcnational.fr
14 |
15 | Parc national des Cevennes
16 | --------------------------
17 |
18 | * Amandine Sahl
19 |
20 | .. image:: http://geonature.fr/img/logo-pnc.jpg
21 | :target: http://www.cevennes-parcnational.fr
22 |
23 | Parc national de la Vanoise
24 | ---------------------------
25 |
26 | * Claire Lagaye
27 |
--------------------------------------------------------------------------------
/app/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/app/templates/group.html:
--------------------------------------------------------------------------------
1 | {% import "wtf_bootstrap_4.html" as wtf %}
2 | {% include "librairies.html" %}
3 | {% include "head-appli.html" %}
4 |
5 | {% block content %}
6 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/app/t_profils/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, SubmitField, HiddenField
3 | from wtforms.validators import DataRequired
4 |
5 |
6 | class Profil(FlaskForm):
7 | """
8 | Classe du formulaire des profils
9 | """
10 |
11 | id_profil = HiddenField("Id")
12 | nom_profil = StringField(
13 | "Nom", validators=[DataRequired(message="Le nom du profil est obligatoire")]
14 | )
15 | code_profil = StringField(
16 | "Code", validators=[DataRequired(message="Le code du profil est obligatoire")]
17 | )
18 | desc_profil = StringField("Description")
19 | submit = SubmitField("Enregistrer")
20 |
--------------------------------------------------------------------------------
/app/templates/scripts.html:
--------------------------------------------------------------------------------
1 |
2 | {% block scripts %}
3 |
4 |
8 |
12 |
16 |
17 |
21 |
22 | {% endblock %}
--------------------------------------------------------------------------------
/app/liste/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, SubmitField, HiddenField, TextAreaField
3 | from wtforms.validators import DataRequired
4 |
5 |
6 | class List(FlaskForm):
7 | """
8 | Classe du formulaire des listes
9 | """
10 |
11 | nom_liste = StringField(
12 | "Nom", validators=[DataRequired(message="Le nom de la liste est obligatoire")]
13 | )
14 | code_liste = StringField(
15 | "Code", validators=[DataRequired(message="Le code de la liste est obligatoire")]
16 | )
17 | desc_liste = TextAreaField("Description")
18 | id_liste = HiddenField("Id")
19 | submit = SubmitField("Enregistrer")
20 |
--------------------------------------------------------------------------------
/app/templates/list.html:
--------------------------------------------------------------------------------
1 | {% import "wtf_bootstrap_4.html" as wtf %}
2 | {% include "librairies.html" %}
3 | {% include "head-appli.html" %}
4 |
5 | {% block content %}
6 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/app/templates/profil.html:
--------------------------------------------------------------------------------
1 | {% import "wtf_bootstrap_4.html" as wtf %}
2 | {% include "librairies.html" %}
3 | {% include "head-appli.html" %}
4 |
5 | {% block content %}
6 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/app/groupe/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, BooleanField, SubmitField, HiddenField, TextAreaField
3 | from wtforms.validators import DataRequired
4 |
5 |
6 | class Group(FlaskForm):
7 | """
8 | Classe du formulaire des Groupes
9 | """
10 |
11 | nom_role = StringField(
12 | "Nom", validators=[DataRequired(message="Le nom du group est obligatoire")]
13 | )
14 | desc_role = TextAreaField("Description")
15 | groupe = BooleanField(
16 | "groupe",
17 | validators=[DataRequired(message="L'information 'groupe' est obligatoire")],
18 | )
19 | id_role = HiddenField("id")
20 | submit = SubmitField("Enregistrer")
21 |
--------------------------------------------------------------------------------
/app/templates/application_role_profil_form.html:
--------------------------------------------------------------------------------
1 |
2 | {% import "wtf_bootstrap_4.html" as wtf %}
3 | {% include "librairies.html" %}
4 | {% include "head-appli.html" %}
5 |
6 |
7 | {% block content %}
8 |
9 |
24 |
25 | {% endblock %}
--------------------------------------------------------------------------------
/app/api/route.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request
2 |
3 |
4 | from app.env import db
5 | from app.utils.utilssqlalchemy import json_resp
6 | from app.models import TProfils, CorProfilForApp
7 |
8 |
9 | route = Blueprint("api", __name__)
10 |
11 |
12 | @route.route("/profils", methods=["GET"])
13 | @json_resp
14 | def get_profils():
15 | """
16 | Return the profils
17 | """
18 | params = request.args
19 | q = db.session.query(TProfils)
20 | if "id_application" in params:
21 | q = q.join(
22 | CorProfilForApp, CorProfilForApp.id_profil == TProfils.id_profil
23 | ).filter(CorProfilForApp.id_application == params["id_application"])
24 | data = [data.as_dict(columns=["id_profil", "nom_profil"]) for data in q.all()]
25 | return data
26 |
--------------------------------------------------------------------------------
/config/config.py.sample:
--------------------------------------------------------------------------------
1 | # Database settings
2 | SQLALCHEMY_DATABASE_URI = "postgresql://monuser:monpassachanger@localhost/usershubdb"
3 | SQLALCHEMY_TRACK_MODIFICATIONS = False
4 | URL_APPLICATION = 'http://localhost:5001'
5 |
6 | SECRET_KEY = 'super secret key'
7 |
8 | # Number of days befoe automatic deletion of account creation request
9 | AUTO_ACCOUNT_DELETION_DAYS = 7
10 |
11 | # Authentification crypting method (hash or md5)
12 | PASS_METHOD = 'hash'
13 |
14 | # Choose if you also want to fill MD5 passwords (lower security)
15 | # Only useful if you have old application that use MD5 passwords
16 | FILL_MD5_PASS = False
17 |
18 | COOKIE_EXPIRATION = 3600
19 | COOKIE_AUTORENEW = True
20 |
21 | # SERVER
22 | PORT = 5001
23 | DEBUG = False
24 |
25 | ACTIVATE_API = True
26 | ACTIVATE_APP = True
27 |
--------------------------------------------------------------------------------
/app/utils/errors.py:
--------------------------------------------------------------------------------
1 | from flask import current_app, Response, request, redirect, url_for
2 | from urllib.parse import urlencode
3 | from werkzeug.exceptions import Unauthorized
4 |
5 |
6 | # Unauthorized means disconnected
7 | # (logged but not allowed to perform an action = Forbidden)
8 |
9 |
10 | def handle_unauthenticated_request():
11 | """
12 | To avoid returning the login page html when a route is used by geonature API
13 | this function overrides `LoginManager.unauthorized()` from `flask-login` .
14 |
15 | Returns
16 | -------
17 | flask.Response
18 | response
19 | """
20 | if "application/json" in request.headers.get("Content-Type", ""):
21 | raise Unauthorized
22 | else:
23 | return redirect(url_for("login.login", next=request.path))
24 |
--------------------------------------------------------------------------------
/app/templates/user_pass.html:
--------------------------------------------------------------------------------
1 |
2 | {% import "wtf_bootstrap_4.html" as wtf %}
3 | {% include "librairies.html" %}
4 | {% include "head-appli.html" %}
5 |
6 |
7 | {% block content %}
8 |
9 |
25 |
26 | {% endblock %}
27 |
--------------------------------------------------------------------------------
/app/templates/generic_table.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% for el in columns %}
7 | {{ el.label }}
8 | {% endfor %}
9 | {% block table_head %}
10 | {% endblock %}
11 |
12 |
13 |
14 | {% for elt in data %}
15 |
16 | {% for col in columns %}
17 | {{ elt[col.key] }}
18 | {% endfor %}
19 | {% block table_td scoped%}
20 | {% endblock %}
21 |
22 | {% endfor %}
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/templates/base.html:
--------------------------------------------------------------------------------
1 | {% block doc %}
2 |
3 |
4 | {% block html %}
5 |
6 | {% block head %}
7 | {% block title %}{{title|default}}{% endblock title %}
8 |
9 | {%- block metas %}
10 |
11 | {% endblock metas %}
12 |
13 | {% block styles %}
14 |
15 | {% endblock styles %}
16 | {% endblock head %}
17 |
18 |
19 | {% block body -%}
20 | {% block navbar %}
21 | {%- endblock navbar %}
22 | {% block content -%}
23 | {%- endblock content %}
24 |
25 | {% block scripts %}
26 |
27 | {% endblock scripts %}
28 | {% endblock body %}
29 |
30 | {% endblock html %}
31 |
32 | {% endblock doc %}
--------------------------------------------------------------------------------
/app/templates/organism.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% import "wtf_bootstrap_4.html" as wtf %}
3 | {% include "head-appli.html" %}
4 |
5 | {% block content %}
6 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/usershub.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=UsersHub
3 | After=network.target
4 | After=postgresql.service
5 |
6 | [Service]
7 | Type=simple
8 | User=${USER}
9 | Group=${USER}
10 | WorkingDirectory=${USERSHUB_DIR}/
11 | Environment=GUNICORN_PROC_NAME=usershub
12 | Environment=GUNICORN_NUM_WORKERS=4
13 | Environment=GUNICORN_HOST=127.0.0.1
14 | Environment=GUNICORN_PORT=5001
15 | Environment=GUNICORN_TIMEOUT=30
16 | Environment=GUNICORN_LOG_FILE=/var/log/usershub/%N%I.log
17 | EnvironmentFile=-${USERSHUB_DIR}/environ
18 | ExecStart=${USERSHUB_DIR}/venv/bin/gunicorn app.app:create_app() \
19 | --name "${GUNICORN_PROC_NAME}" --workers "${GUNICORN_NUM_WORKERS}" \
20 | --bind "${GUNICORN_HOST}:${GUNICORN_PORT}" --timeout="${GUNICORN_TIMEOUT}" \
21 | --log-file "${GUNICORN_LOG_FILE}"
22 | ExecReload=/bin/kill -s HUP $MAINPID
23 | TimeoutStartSec=10
24 | TimeoutStopSec=5
25 | PrivateTmp=true
26 |
27 | [Install]
28 | WantedBy=multi-user.target
29 |
--------------------------------------------------------------------------------
/app/static/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "usershub",
3 | "version": "2.0.0",
4 | "description": "Gestion centralisée des utilisateurs",
5 | "main": "index.js",
6 | "directories": {
7 | "doc": "docs"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/PnEcrins/UsersHub.git"
15 | },
16 | "keywords": [
17 | "PnX",
18 | "Geonature",
19 | "Biodiversité",
20 | "Parcs",
21 | "nationaux"
22 | ],
23 | "author": "",
24 | "license": "GPL-3.0",
25 | "bugs": {
26 | "url": "https://github.com/PnEcrins/UsersHub/issues"
27 | },
28 | "homepage": "https://github.com/PnEcrins/UsersHub#readme",
29 | "dependencies": {
30 | "bootstrap": "^4.5.0",
31 | "datatables.net": "^1.11.3",
32 | "font-awesome": "^4.7.0",
33 | "footable": "^2.0.6",
34 | "jquery": "^3.5.0",
35 | "popper.js": "^1.14.3"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 | version_locations = %(here)s/versions
12 |
13 |
14 | # Logging configuration
15 | [loggers]
16 | keys = root,sqlalchemy,alembic
17 |
18 | [handlers]
19 | keys = console
20 |
21 | [formatters]
22 | keys = generic
23 |
24 | [logger_root]
25 | level = WARN
26 | handlers = console
27 | qualname =
28 |
29 | [logger_sqlalchemy]
30 | level = WARN
31 | handlers =
32 | qualname = sqlalchemy.engine
33 |
34 | [logger_alembic]
35 | level = INFO
36 | handlers =
37 | qualname = alembic
38 |
39 | [handler_console]
40 | class = StreamHandler
41 | args = (sys.stderr,)
42 | level = NOTSET
43 | formatter = generic
44 |
45 | [formatter_generic]
46 | format = %(levelname)-5.5s [%(name)s] %(message)s
47 | datefmt = %H:%M:%S
48 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.12"
12 | # You can also specify other tool versions:
13 | # nodejs: "20"
14 | # rust: "1.70"
15 | # golang: "1.20"
16 |
17 | # Build documentation in the "docs/" directory with Sphinx
18 | sphinx:
19 | configuration: docs/conf.py
20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
21 | # builder: "dirhtml"
22 | # Fail on all warnings to avoid broken references
23 | # fail_on_warning: true
24 |
25 | # Optionally build your docs in additional formats such as PDF and ePub
26 | # formats:
27 | # - pdf
28 | # - epub
29 |
30 | # Optional but recommended, declare the Python requirements required
31 | # to build your documentation
32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
33 | python:
34 | install:
35 | - requirements: docs/requirements.readthedocs.txt
36 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 | from pathlib import Path
3 |
4 |
5 | root_dir = Path(__file__).absolute().parent
6 | with (root_dir / "VERSION").open() as f:
7 | version = f.read()
8 | with (root_dir / "README.rst").open() as f:
9 | long_description = f.read()
10 |
11 |
12 | setuptools.setup(
13 | name="usershub",
14 | description="Application web de gestion centralisée des utilisateurs",
15 | long_description=long_description,
16 | long_description_content_type="text/x-rst",
17 | maintainer="Parcs nationaux des Écrins et des Cévennes",
18 | maintainer_email="geonature@ecrins-parcnational.fr",
19 | url="https://github.com/PnX-SI/UsersHub",
20 | version=version,
21 | packages=setuptools.find_packages(where=".", include=["app*"]),
22 | install_requires=(
23 | list(open("requirements-common.in", "r"))
24 | + list(open("requirements-dependencies.in", "r"))
25 | ),
26 | package_data={
27 | "app": ["templates/*.html", "templates/*.js"],
28 | "app.migrations": ["alembic.ini", "script.py.mako"],
29 | },
30 | entry_points={
31 | "alembic": [
32 | "migrations = app.migrations:versions",
33 | ],
34 | },
35 | )
36 |
--------------------------------------------------------------------------------
/app/templates/user.html:
--------------------------------------------------------------------------------
1 |
2 | {% include "librairies.html" %}
3 | {% import "wtf_bootstrap_4.html" as wtf %}
4 | {% include "head-appli.html" %}
5 |
6 |
7 | {% block content %}
8 |
9 |
33 |
34 | {% endblock %}
35 |
--------------------------------------------------------------------------------
/app/bib_organismes/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import (
3 | StringField,
4 | SubmitField,
5 | HiddenField,
6 | validators,
7 | )
8 | from wtforms.validators import DataRequired, Email
9 |
10 |
11 | def validate_code_postal(form, field):
12 | if len(field.data) != 5:
13 | raise validators.ValidationError(
14 | "Le code postal renseigné comporte plus/moins de 5 caractères"
15 | )
16 |
17 |
18 | class Organisme(FlaskForm):
19 | """
20 | Classe du formulaire des Organismes
21 | """
22 |
23 | nom_organisme = StringField(
24 | "Nom de l'organisme",
25 | validators=[DataRequired(message="Le nom de l'organisme est obligatoire")],
26 | )
27 | adresse_organisme = StringField("Adresse")
28 | cp_organisme = StringField("Code Postal", validators=[validate_code_postal])
29 | ville_organisme = StringField("Ville")
30 | tel_organisme = StringField("Téléphone")
31 | fax_organisme = StringField("Fax")
32 | email_organisme = StringField(
33 | "E-mail",
34 | validators=[validators.Optional(), Email(message="L'email est incorect")],
35 | )
36 | url_organisme = StringField("URL du site web de l'organisme")
37 | url_logo = StringField("Logo (URL)")
38 | id_organisme = HiddenField("id")
39 | submit = SubmitField("Enregistrer")
40 |
--------------------------------------------------------------------------------
/config/settings.ini.sample:
--------------------------------------------------------------------------------
1 | ###########################
2 | ### PostgreSQL settings ###
3 | ###########################
4 |
5 | # set to dev to install usershub in development mode
6 | mode=prod
7 |
8 | # Effacer la base de donnée existante lors de la réinstallation
9 | drop_apps_db=false
10 | # Host de la base de données de l'application
11 | db_host=localhost
12 | # Port du serveur PostgreSQL
13 | pg_port=5432
14 | # Nom de la base de données de l'application
15 | db_name=usershubdb
16 | # Nom de l'utilisateur propriétaire de la BDD de l'application
17 | user_pg=geonatuser
18 | # User propriétaire de la BDD de l'application
19 | user_pg_pass=monpassachanger
20 | # Intégrer les données minimales (Applications et tags)
21 | insert_minimal_data=true
22 | # Intégrer les données exemple (Role, groupe, organismes et correspondances)
23 | insert_sample_data=true
24 |
25 | ############################
26 | ### Application settings ###
27 | ############################
28 |
29 | # URL de l'application
30 | url_application=http://mon_url.fr
31 |
32 |
33 | #######################
34 | ### Python settings ###
35 | #######################
36 |
37 | # Note : n'est compatible qu'avec python3
38 | python_path=/usr/bin/python3
39 |
40 | #########################
41 | ### Gunicorn settings ###
42 | #########################
43 |
44 | app_name=usershub2
45 | venv_dir=venv
46 | gun_num_workers=4
47 | gun_host=0.0.0.0
48 | gun_port=5001
49 |
--------------------------------------------------------------------------------
/install_db.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Make sure root cannot run our script
3 | if [ "$(id -u)" == "0" ]; then
4 | echo "This script must NOT be run as root" 1>&2
5 | exit 1
6 | fi
7 |
8 | . config/settings.ini
9 |
10 | function database_exists () {
11 | # /!\ Will return false if psql can't list database. Edit your pg_hba.conf
12 | # as appropriate.
13 | if [ -z $1 ]
14 | then
15 | # Argument is null
16 | return 0
17 | else
18 | # Grep db name in the list of database
19 | PGPASSWORD=$user_pg_pass psql -h $db_host -p $pg_port -U $user_pg -tAl | grep -q "^$1|"
20 | return $?
21 | fi
22 | }
23 |
24 | if database_exists $db_name
25 | then
26 | if $drop_apps_db
27 | then
28 | echo "Suppression de la base..."
29 | PGPASSWORD=$user_pg_pass dropdb -U $user_pg -h $db_host -p $pg_port $db_name
30 | else
31 | echo "La base de données existe et le fichier de settings indique de ne pas la supprimer."
32 | fi
33 | fi
34 |
35 | if ! database_exists $db_name
36 | then
37 | mkdir -p log
38 | echo "Création de la base..."
39 | PGPASSWORD=$user_pg_pass createdb -h $db_host -p $pg_port -U $user_pg -O $user_pg $db_name
40 | echo "Ajout de l'extension pour les UUID..."
41 | PGPASSWORD=$user_pg_pass psql -h $db_host -p $pg_port -U $user_pg -d $db_name -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'
42 | fi
43 |
44 |
45 | source venv/bin/activate
46 | flask db upgrade usershub@head
47 | flask db upgrade utilisateurs@head
48 | deactivate
49 |
--------------------------------------------------------------------------------
/app/migrations/versions/f63a8f44c969_usershub_samples.py:
--------------------------------------------------------------------------------
1 | """UsersHub samples data
2 |
3 | Revision ID: f63a8f44c969
4 | Revises:
5 | Create Date: 2021-09-06 18:17:06.392398
6 |
7 | """
8 |
9 | from alembic import op
10 | import sqlalchemy as sa
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = "f63a8f44c969"
15 | down_revision = None
16 | branch_labels = ("usershub-samples",)
17 | depends_on = (
18 | "9445a69f2bed", # usershub
19 | "72f227e37bdf", # utilisateurs schema samples data
20 | )
21 |
22 |
23 | def upgrade():
24 | op.execute(
25 | """
26 | INSERT INTO utilisateurs.cor_role_app_profil (id_role, id_application, id_profil) VALUES
27 | (
28 | (SELECT id_role FROM utilisateurs.t_roles WHERE nom_role = 'Grp_admin'),
29 | (SELECT id_application FROM utilisateurs.t_applications WHERE code_application = 'UH'),
30 | (SELECT id_profil FROM utilisateurs.t_profils WHERE code_profil = '6')
31 | )
32 | """
33 | )
34 |
35 |
36 | def downgrade():
37 | op.execute(
38 | """
39 | DELETE FROM utilisateurs.cor_role_app_profil cor
40 | USING
41 | utilisateurs.t_roles r,
42 | utilisateurs.t_applications a,
43 | utilisateurs.t_profils p
44 | WHERE
45 | cor.id_role = r.id_role
46 | AND cor.id_application = a.id_application
47 | AND cor.id_profil = p.id_profil
48 | AND r.nom_role = 'Grp_admin'
49 | AND a.code_application = 'UH'
50 | AND p.code_profil = '6'
51 | """
52 | )
53 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 | release:
8 | types: [published]
9 |
10 | jobs:
11 | docker:
12 | runs-on: ubuntu-latest
13 | steps:
14 | -
15 | name: Checkout
16 | uses: actions/checkout@v3
17 | with:
18 | submodules: ${{ ! (github.event_name == 'release' && github.event.action == 'published') }}
19 | -
20 | name: Set up Docker Buildx
21 | uses: docker/setup-buildx-action@v2
22 | -
23 | name: Docker meta
24 | id: meta
25 | uses: docker/metadata-action@v4
26 | with:
27 | images: ghcr.io/pnx-si/usershub
28 | tags: |
29 | type=ref,event=branch
30 | type=ref,event=tag
31 | -
32 | name: Login to GHCR
33 | uses: docker/login-action@v2
34 | if: github.event_name != 'pull_request'
35 | with:
36 | registry: ghcr.io
37 | username: ${{ github.actor }}
38 | password: ${{ secrets.GITHUB_TOKEN }}
39 | -
40 | name: Build and export
41 | uses: docker/build-push-action@v3
42 | with:
43 | context: .
44 | target: prod
45 | build-args:
46 | DEPS=${{ (github.event_name == 'release' && github.event.action == 'published') && 'pypi' || 'build' }}
47 | push: ${{ github.event_name != 'pull_request' }}
48 | tags: ${{ steps.meta.outputs.tags }}
49 | labels: ${{ steps.meta.outputs.labels }}
50 |
--------------------------------------------------------------------------------
/app/templates/application.html:
--------------------------------------------------------------------------------
1 | {% import "wtf_bootstrap_4.html" as wtf %}
2 | {% include "librairies.html" %}
3 | {% include "head-appli.html" %}
4 |
5 | {% block content %}
6 |
45 | {% endblock %}
46 |
--------------------------------------------------------------------------------
/app/templates/temp_users.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 | {% include "alert_messages.html" %}
4 |
5 |
6 |
7 |
Demandes de compte en cours
8 |
9 |
10 |
11 |
12 |
13 | {% extends "generic_table.html" %}
14 |
15 |
16 | {% block table_head %}
17 |
18 |
20 |
23 |
24 |
25 |
27 |
30 |
31 |
32 | {% endblock table_head %}
33 |
34 |
35 | {% block table_td %}
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {% endblock table_td %}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/app/migrations/versions/9445a69f2bed_usershub.py:
--------------------------------------------------------------------------------
1 | """UsersHub
2 |
3 | Revision ID: 9445a69f2bed
4 | Create Date: 2021-08-30 16:33:42.410504
5 |
6 | """
7 |
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = "9445a69f2bed"
14 | down_revision = None
15 | branch_labels = ("usershub",)
16 | depends_on = ("fa35dfe5ff27",) # schema utilisateurs
17 |
18 |
19 | def upgrade():
20 | op.execute(
21 | """
22 | INSERT INTO utilisateurs.t_applications (
23 | code_application,
24 | nom_application,
25 | desc_application,
26 | id_parent)
27 | VALUES (
28 | 'UH',
29 | 'UsersHub',
30 | 'Application permettant d''administrer la présente base de données.',
31 | NULL)
32 | """
33 | )
34 | op.execute(
35 | """
36 | INSERT INTO utilisateurs.cor_profil_for_app
37 | (id_profil, id_application)
38 | VALUES
39 | (
40 | (SELECT id_profil FROM utilisateurs.t_profils WHERE code_profil = '6'),
41 | (SELECT id_application FROM utilisateurs.t_applications WHERE code_application = 'UH')
42 | ), (
43 | (SELECT id_profil FROM utilisateurs.t_profils WHERE code_profil = '3'),
44 | (SELECT id_application FROM utilisateurs.t_applications WHERE code_application = 'UH')
45 | )
46 | """
47 | )
48 |
49 |
50 | def downgrade():
51 | op.execute(
52 | """
53 | DELETE FROM utilisateurs.cor_profil_for_app cor
54 | USING utilisateurs.t_applications app
55 | WHERE cor.id_application = app.id_application
56 | AND app.code_application = 'UH'
57 | """
58 | )
59 | op.execute("DELETE FROM utilisateurs.t_applications WHERE code_application = 'UH'")
60 |
--------------------------------------------------------------------------------
/app/static/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "usershub",
3 | "version": "2.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "bootstrap": {
8 | "version": "4.5.0",
9 | "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz",
10 | "integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA=="
11 | },
12 | "datatables.net": {
13 | "version": "1.11.3",
14 | "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.3.tgz",
15 | "integrity": "sha512-VMj5qEaTebpNurySkM6jy6sGpl+s6onPK8xJhYr296R/vUBnz1+id16NVqNf9z5aR076OGcpGHCuiTuy4E05oQ==",
16 | "requires": {
17 | "jquery": ">=1.7"
18 | }
19 | },
20 | "font-awesome": {
21 | "version": "4.7.0",
22 | "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
23 | "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
24 | },
25 | "footable": {
26 | "version": "2.0.6",
27 | "resolved": "https://registry.npmjs.org/footable/-/footable-2.0.6.tgz",
28 | "integrity": "sha1-JOLQxKYWSOA/wDE4F76FOqWcCdA=",
29 | "requires": {
30 | "jquery": ">=1.4.4"
31 | }
32 | },
33 | "jquery": {
34 | "version": "3.5.0",
35 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz",
36 | "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ=="
37 | },
38 | "popper.js": {
39 | "version": "1.16.1",
40 | "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
41 | "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/t_roles/forms.py:
--------------------------------------------------------------------------------
1 | """
2 | Définition du formulaire : création/modification d'un role
3 | """
4 |
5 | from flask_wtf import FlaskForm
6 | from wtforms import (
7 | StringField,
8 | PasswordField,
9 | SubmitField,
10 | HiddenField,
11 | SelectField,
12 | RadioField,
13 | BooleanField,
14 | SelectMultipleField,
15 | TextAreaField,
16 | widgets,
17 | validators,
18 | )
19 | from wtforms.validators import DataRequired, Email
20 |
21 |
22 | class MultiCheckboxField(SelectMultipleField):
23 | widget = widgets.ListWidget(prefix_label=False)
24 | option_widget = widgets.CheckboxInput()
25 |
26 |
27 | class Utilisateur(FlaskForm):
28 | active = BooleanField("Actif", default=True, false_values=(False, "false"))
29 | nom_role = StringField(
30 | "Nom",
31 | validators=[DataRequired(message="Le nom de l'utilisateur est obligatoire")],
32 | )
33 | prenom_role = StringField("Prenom")
34 | desc_role = TextAreaField("Description")
35 | id_organisme = SelectField("Organisme", coerce=int, choices=[], default=-1)
36 | a_groupe = SelectMultipleField("", choices=[], coerce=int)
37 | identifiant = StringField("Identifiant")
38 | pass_plus = PasswordField("Mot de passe")
39 | mdpconf = PasswordField("Confirmation")
40 | email = StringField(
41 | "E-mail",
42 | validators=[validators.Optional(), Email(message="L'email est incorect")],
43 | )
44 | groupe = HiddenField("groupe", default=None)
45 | remarques = TextAreaField("Commentaire")
46 | id_role = HiddenField("id")
47 | submit = SubmitField("Enregistrer")
48 |
49 |
50 | class UserPass(FlaskForm):
51 | pass_plus = PasswordField("Mot de passe")
52 | mdpconf = PasswordField("Confirmation")
53 | id_role = HiddenField("id")
54 | submit = SubmitField("Enregistrer")
55 |
--------------------------------------------------------------------------------
/app/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 | {% include "librairies.html" %}
3 |
4 |
5 |
6 |
7 |
UsersHub
8 |
9 |
10 | Paramètres de connexion invalides
11 |
12 |
22 |
23 |
24 |
25 |
26 |
58 |
--------------------------------------------------------------------------------
/app/templates/info_list.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 |
4 | {%set is_members = members|length > 0 %}
5 |
6 |
Liste "{{mylist['nom_liste']}}"
7 |
{{mylist['desc_liste']}}
8 |
9 |
10 |
11 |
12 |
15 |
16 | {% if is_members %}
17 |
31 | {% else %}
32 |
Le groupe ne comporte aucun membre.
33 | {% endif %}
34 |
35 |
36 |
37 |
38 |
39 | Modifier la liste
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/templates/head-appli.html:
--------------------------------------------------------------------------------
1 |
62 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | postgres:
3 | image: postgis/postgis:13-3.2
4 | environment:
5 | - POSTGRES_DB=${POSTGRES_DB:-usershub}
6 | - POSTGRES_USER=${POSTGRES_USER:-usershubadmin}
7 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-usershubpasswd}
8 | volumes:
9 | - ./db:/docker-entrypoint-initdb.d/
10 | - postgres:/var/lib/postgresql/data
11 | healthcheck:
12 | # during db init, postgres is not listening on localhost so this avoid false healthy status
13 | test:
14 | [
15 | "CMD",
16 | "pg_isready",
17 | "-d",
18 | "${POSTGRES_DB}",
19 | "-U",
20 | "${POSTGRES_USER}",
21 | "-h",
22 | "localhost",
23 | ]
24 | interval: 10s
25 | timeout: 5s
26 | retries: 5
27 |
28 | usershub-install-db:
29 | image: pnx-si/usershub:latest
30 | build: .
31 | depends_on:
32 | postgres:
33 | condition: service_healthy
34 | environment:
35 | - USERSHUB_URL_APPLICATION=${USERSHUB_URL_APPLICATION:-/}
36 | - USERSHUB_SETTINGS=${USERSHUB_SETTINGS:-config.py}
37 | - USERSHUB_SQLALCHEMY_DATABASE_URI=postgresql://${POSTGRES_USER:-usershubadmin}:${POSTGRES_PASSWORD:-usershubpasswd}@postgres:5432/${POSTGRES_DB:-usershub}
38 | command: sh -c "python -m flask db upgrade utilisateurs@head && python -m flask db upgrade usershub@head && python -m flask db upgrade usershub-samples@head"
39 | working_dir: /dist
40 |
41 | usershub:
42 | image: pnx-si/usershub:latest
43 | build: .
44 | depends_on:
45 | postgres:
46 | condition: service_healthy
47 | usershub-install-db:
48 | condition: service_completed_successfully
49 | volumes:
50 | - ./config/:/dist/config/
51 | user: ${UID:-1000}:${GID:-1000}
52 | environment:
53 | - USERSHUB_URL_APPLICATION=${USERSHUB_URL_APPLICATION:-http://localhost:5001}
54 | - USERSHUB_SECRET_KEY=${USERSHUB_SECRET_KEY:-mysupersecretkey}
55 | - USERSHUB_SETTINGS=${USERSHUB_SETTINGS:-config.py}
56 | - USERSHUB_SQLALCHEMY_DATABASE_URI=postgresql://${POSTGRES_USER:-usershubadmin}:${POSTGRES_PASSWORD:-usershubpasswd}@postgres:5432/${POSTGRES_DB:-usershub}
57 | - PYTHONPATH=/dist/config
58 | ports:
59 | - "${USERSHUB_PORT:-5001}:5001"
60 |
61 | volumes:
62 | postgres:
63 |
--------------------------------------------------------------------------------
/docs/migration-v1v2.rst:
--------------------------------------------------------------------------------
1 | =================
2 | MIGRATION V1 > V2
3 | =================
4 |
5 | Procédure de mise à jour de UsersHub version 1 vers la version 2.0.0
6 |
7 | * Télécharger la dernière version de UsersHub
8 |
9 | ::
10 |
11 | cd
12 | wget https://github.com/PnEcrins/UsersHub/archive/X.Y.Z.zip
13 | unzip X.Y.Z.zip
14 |
15 | Renommer l’ancien repertoire de l’application, ainsi que le nouveau :
16 |
17 | ::
18 |
19 | mv /home/`whoami`/usershub/ /home/`whoami`/usershub_old/
20 | mv UsersHub-X.Y.Z /home/`whoami`/usershub/
21 |
22 | * Créer et mettre à jour le fichier ``config/settings.ini``.
23 |
24 | Remplir uniquement la partie 'PostgreSQL settings' et 'Application settings', avec les paramètres de connexion de la base de données contenant votre schéma ``utilisateurs``. Dans notre cas, il s'agit de la base de données de GeoNature.
25 |
26 | ::
27 |
28 | cd usershub
29 | cp config/settings.ini.sample config/settings.ini
30 | nano config/settings.ini
31 |
32 | Exemple :
33 |
34 | ::
35 |
36 | # Effacer la base de donnée existante lors de l'installation
37 | drop_apps_db=false
38 | # Host de la base de données de l'application
39 | db_host=localhost
40 | # Port du serveur PostgreSQL
41 | pg_port=5432
42 | # Nom de la base de données de l'application
43 | db_name=geonature2db
44 | # Nom de l'utilisateur propriétaire de la BDD de l'application
45 | user_pg=geonatadmin
46 | # User propriétaire de la BDD de l'application
47 | user_pg_pass=monpassachanger
48 | # Intégrer les données minimales (Applications et tags)
49 | insert_minimal_data=true
50 | # Intégrer les données exemple (Role, groupe, organismes et correspondances)
51 | insert_sample_data=true
52 | # URL de l'application
53 | url_application=http://test.ecrins-parcnational.net/usershub
54 |
55 |
56 | Passer le script de migration ``data/update_1.3.3to2.0.0.sql``
57 |
58 | Lancer le script d'installation de l'application :
59 |
60 | ::
61 |
62 | ./install_app.sh
63 |
64 |
65 | * Configuration Apache
66 |
67 | Supprimer le contenu du fichier ``/etc/apache2/sites-enabled/usershub.conf`` et le remplacer par les lignes suivantes :
68 |
69 | ::
70 |
71 |
72 | ProxyPass http://localhost:5001/
73 | ProxyPassReverse http://localhost:5001/
74 |
75 |
76 | Redémarer Apache
77 |
78 | ::
79 |
80 | sudo service apache2 restart
81 |
--------------------------------------------------------------------------------
/app/templates/librairies.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}UsersHub V2{% endblock %}
4 |
5 | {% block styles %}
6 |
7 |
56 |
60 |
64 |
68 |
72 |
76 | {% endblock styles%}
77 |
78 |
79 | {% block scripts %}
80 |
84 |
88 |
92 |
93 |
97 |
98 |
99 | {% endblock scripts %}
100 |
--------------------------------------------------------------------------------
/.github/workflows/test_install.yml:
--------------------------------------------------------------------------------
1 | name: Test the installation of UsersHub
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | setup:
7 | runs-on: ubuntu-latest
8 | services:
9 | postgres:
10 | image: postgis/postgis:15-3.4
11 | env:
12 | POSTGRES_DB: usershub_test
13 | POSTGRES_USER: postgres
14 | POSTGRES_PASSWORD: postgres
15 | ports:
16 | - 5432:5432
17 | options: >-
18 | --health-cmd pg_isready
19 | --health-interval 10s
20 | --health-timeout 5s
21 | --health-retries 5
22 | steps:
23 | - uses: actions/checkout@v4
24 | with:
25 | submodules: true
26 | - name: Set up Python 3.9
27 | uses: actions/setup-python@v5
28 | with:
29 | python-version: 3.9
30 | cache: "pip"
31 | - name: Fill settings file
32 | run: |
33 | cp config/settings.ini.sample config/settings.ini
34 | cp config/config.py.sample config/config.py
35 | sed -i 's|SQLALCHEMY_DATABASE_URI = .*|SQLALCHEMY_DATABASE_URI = "postgresql://postgres:postgres@localhost:5432/usershub_test"|' config/config.py
36 | sed -i 's|SECRET_KEY = .*|SECRET_KEY = "mysupersecretkey"|' config/config.py
37 | sed -i 's|user_pg=.*|user_pg=postgres|' config/settings.ini
38 | sed -i 's|user_pg_pass=.*|user_pg_pass=postgres|' config/settings.ini
39 | sed -i 's|db_name=.*|db_name=usershub_test|' config/settings.ini
40 | sed -i 's|db_host=.*|db_host=localhost|' config/settings.ini
41 | sed -i 's|pg_port=.*|pg_port=5432|' config/settings.ini
42 | cat config/settings.ini
43 | cat config/config.py
44 | - name: Install database extensions
45 | run: |
46 | PGPASSWORD=postgres psql -U postgres -d usershub_test -h localhost -p 5432 -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'
47 | - name: Install dependencies
48 | run: |
49 | ./install_app.sh
50 | ./install_db.sh
51 | source venv/bin/activate
52 | flask db upgrade usershub-samples@head
53 | - name: Run UsersHub
54 | run: |
55 | source venv/bin/activate
56 | flask run &
57 | - name: Check if UsersHub is running
58 | run: |
59 | CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5001/login)
60 | if [ $CODE != 200 ]; then
61 | echo "Error: UsersHub is not running properly. Status code is $CODE"
62 | exit 1
63 | fi
64 |
--------------------------------------------------------------------------------
/app/t_applications/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, BooleanField, SubmitField, HiddenField, SelectField
3 | from wtforms.validators import DataRequired
4 |
5 | from app.models import TRoles, TProfils, CorRoleAppProfil
6 | from app.env import db
7 |
8 |
9 | class Application(FlaskForm):
10 | """
11 | Classe du formulaire des applications
12 | """
13 |
14 | nom_application = StringField(
15 | "Nom",
16 | validators=[DataRequired(message="Le nom de l'application est obligatoire")],
17 | )
18 | code_application = StringField(
19 | "Code",
20 | validators=[DataRequired(message="Le code de l'application est obligatoire")],
21 | )
22 | desc_application = StringField("Description")
23 | id_parent = SelectField("Application parent", coerce=int, choices=[])
24 | id_application = HiddenField("id")
25 | submit = SubmitField("Enregistrer")
26 |
27 |
28 | class AppProfil(FlaskForm):
29 | """
30 | Classe du formulaire de profil d'un role pour une appli
31 | """
32 |
33 | role = SelectField("Role", coerce=int)
34 | profil = SelectField("Profil", coerce=int)
35 | profil = SelectField("Profil", coerce=int)
36 |
37 | submit = SubmitField("Enregistrer")
38 |
39 | # on surchage le constructeur de la classe pour y passer l'id_application en paramètre
40 | # et filter les listes déroulante des profils et des roles
41 | def __init__(self, id_application, *args, **kwargs):
42 | super(AppProfil, self).__init__(*args, **kwargs)
43 | # on ne met que les users qui n'ont pas déja un profil dans l'app
44 | # sous requete utilisateurs ayant deja un profil pour une app
45 | user_with_profil_in_app = db.session.query(CorRoleAppProfil.id_role).filter(
46 | CorRoleAppProfil.id_application == id_application
47 | )
48 | users_no_profils_in_app = (
49 | db.session.query(TRoles)
50 | .filter(TRoles.id_role.notin_(user_with_profil_in_app))
51 | .order_by(TRoles.nom_role.asc())
52 | .all()
53 | )
54 | users_select_choices = []
55 | for user in users_no_profils_in_app:
56 | user_as_dict = user.as_dict_full_name()
57 | users_select_choices.append(
58 | (user_as_dict["id_role"], user_as_dict["full_name"])
59 | )
60 | self.role.choices = users_select_choices
61 | # choix des profils dispo pour une appli
62 | self.profil.choices = TProfils.choixSelect(id_application=id_application)
63 |
--------------------------------------------------------------------------------
/install_app.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | . config/settings.ini || exit 1
4 |
5 | # Création des fichiers de configuration
6 | cd config
7 |
8 |
9 | echo "Création du fichier de configuration ..."
10 | if [ ! -f config.py ]; then
11 | cp config.py.sample config.py || exit 1
12 |
13 | echo "préparation du fichier config.py..."
14 | sed -i "s/SQLALCHEMY_DATABASE_URI = .*$/SQLALCHEMY_DATABASE_URI = \"postgresql:\/\/$user_pg:$user_pg_pass@$db_host:$pg_port\/$db_name\"/" config.py || exit 1
15 |
16 | url_application="${url_application//\//\\/}"
17 | # on enleve le / final
18 | if [ "${url_application: -1}" = '/' ]
19 | then
20 | url_application="${url_application::-1}"
21 | fi
22 | sed -i "s/URL_APPLICATION =.*$/URL_APPLICATION ='$url_application'/g" config.py || exit 1
23 | fi
24 |
25 | cd ..
26 |
27 | # Installation de l'environement python
28 |
29 | echo "Installation du virtual env..."
30 | python3 -m venv venv || exit 1
31 | source venv/bin/activate
32 | pip install --upgrade pip || exit 1
33 | if [ "${mode}" = "dev" ]; then
34 | pip install -r requirements-dev.txt || exit 1
35 | else
36 | pip install -r requirements.txt || exit 1
37 | fi
38 | pip install -e . || exit 1
39 | deactivate
40 |
41 | # rendre la commande nvm disponible
42 | export NVM_DIR="$HOME/.nvm"
43 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
44 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
45 | # Installation de l'environement javascript
46 | cd app/static
47 | nvm install || exit 1
48 | nvm use || exit 1
49 | npm ci || exit 1
50 | cd ../..
51 |
52 | if [ "${mode}" != "dev" ]; then
53 | #Lancement de l'application
54 | export USERSHUB_DIR=$(readlink -e "${0%/*}")
55 |
56 | # Configuration systemd
57 | envsubst '${USER}' < tmpfiles-usershub.conf | sudo tee /etc/tmpfiles.d/usershub.conf || exit 1
58 | sudo systemd-tmpfiles --create /etc/tmpfiles.d/usershub.conf || exit 1
59 | envsubst '${USER} ${USERSHUB_DIR}' < usershub.service | sudo tee /etc/systemd/system/usershub.service || exit 1
60 | sudo systemctl daemon-reload || exit 1
61 |
62 | # Configuration logrotate
63 | envsubst '${USER}' < log_rotate | sudo tee /etc/logrotate.d/usershub
64 |
65 | # Configuration apache
66 | sudo cp usershub_apache.conf /etc/apache2/conf-available/usershub.conf || exit 1
67 | sudo a2enmod proxy || exit 1
68 | sudo a2enmod proxy_http || exit 1
69 | # you may need a restart if proxy & proxy_http was not already enabled
70 |
71 | echo "Vous pouvez maintenant démarrer UsersHub avec la commande : sudo systemctl start usershub"
72 | fi;
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | UsersHub
2 | =========
3 |
4 | Application web de gestion centralisée des utilisateurs.
5 |
6 | UsersHub est une application web permettant de regrouper l'ensemble des utilisateurs d'applications web afin de gérer de manière différenciée et centralisée les droits d'accès à ces applications ainsi que le contenu des listes déroulantes d'observateurs.
7 |
8 | Elle permet de gérer de manière centralisée des **utilisateurs** et de les placer dans des **groupes**, de créer des **profils** et de les affecter aux utilisateurs et/ou aux groupes d'utilisateurs pour chacune de vos **applications**. Elle permet également de gérer des **organismes**.
9 |
10 | Compatible avec GeoNature (https://github.com/PNX-SI/GeoNature), TaxHub (https://github.com/PnX-SI/TaxHub), Police (https://github.com/PnEcrins/Police) et Geotrek (https://github.com/GeotrekCE/Geotrek-admin).
11 |
12 | Présentation
13 | -----------
14 |
15 | Principe général : UsersHub permet de gérer et de synchroniser le contenu d'un ou plusieurs schéma ``utilisateurs`` d'une ou plusieurs bases PostgreSQL. A condition que le modèle mais aussi que toutes les données de ces bases soient identiques, UsersHub permet de maintenir le contenu du schéma ``utilisateurs`` de ces bases strictement identique.
16 |
17 | Dans un système d'information, les applications web 'métier' nécessitent généralement une identification par login/pass.
18 | Les applications disposent donc d'un dispositif de gestion des utilisateurs et de leur droits.
19 |
20 | L'utilisateur n'a pas forcement les mêmes droits d'une application à l'autre et l'administrateur doit maintenir une liste d'utilisateurs dans chacune des applications. Ces applications peuvent avoir chacune une base de données dédiée.
21 |
22 | A condition d'organiser la gestion de ces utilisateurs de manière identique dans toutes les bases des applications web, UsersHub permet de centraliser cette gestion et de réaliser les modification dans toutes les bases filles avec un mécanisme de synchronisation des schémas ``utilisateurs``.
23 |
24 | Si un utilisateur arrivent dans votre structure, si un mot de passe doit être changé, vous ne le faites qu'une seule fois.
25 |
26 | Une fois enregistré, un utilisateur peut être placé dans un groupe et ses droits d'accès à telle ou telle application web sont hérités des droits du groupe.
27 |
28 | Mais vous pouvez aussi affecter des droits spécifiques à un utilisateur pour telle application ou telle autre.
29 |
30 | Si certains des utilisateurs ou groupe d'utilisateurs doivent figurer dans une liste déroulante de l'application (par exemple une liste d'observateurs ou de représentants), UsersHub permet de créer ces listes et d'en gérer le contenu.
31 |
32 | Il ne vous reste alors plus qu'à utiliser cette liste dans votre application.
33 |
34 | .. image :: http://geonature.fr/img/uhv2-screenshot.jpg
35 |
36 | Installation
37 | -----------
38 |
39 | Consulter la documentation : ``_
40 |
41 | Ou dans ``docs/installation.rst``
42 |
43 | License
44 | -------
45 |
46 | * OpenSource - GPLv3
47 | * Copyright (c) 2015-2018 - Parc National des Écrins
48 |
49 |
50 | .. image:: http://geonature.fr/img/logo-pne.jpg
51 | :target: http://www.ecrins-parcnational.fr
52 |
--------------------------------------------------------------------------------
/app/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 |
11 | # this is the Alembic Config object, which provides
12 | # access to the values within the .ini file in use.
13 | config = context.config
14 |
15 | # Interpret the config file for Python logging.
16 | # This line sets up loggers basically.
17 | fileConfig(config.config_file_name)
18 | logger = logging.getLogger("alembic.env")
19 |
20 | # add your model's MetaData object here
21 | # for 'autogenerate' support
22 | # from myapp import mymodel
23 | # target_metadata = mymodel.Base.metadata
24 | from flask import current_app
25 |
26 | config.set_main_option(
27 | "sqlalchemy.url",
28 | str(current_app.extensions["migrate"].db.engine.url).replace("%", "%%"),
29 | )
30 | target_metadata = current_app.extensions["migrate"].db.metadata
31 |
32 | # other values from the config, defined by the needs of env.py,
33 | # can be acquired:
34 | # my_important_option = config.get_main_option("my_important_option")
35 | # ... etc.
36 |
37 |
38 | def run_migrations_offline():
39 | """Run migrations in 'offline' mode.
40 |
41 | This configures the context with just a URL
42 | and not an Engine, though an Engine is acceptable
43 | here as well. By skipping the Engine creation
44 | we don't even need a DBAPI to be available.
45 |
46 | Calls to context.execute() here emit the given string to the
47 | script output.
48 |
49 | """
50 | url = config.get_main_option("sqlalchemy.url")
51 | context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
52 |
53 | with context.begin_transaction():
54 | context.run_migrations()
55 |
56 |
57 | def run_migrations_online():
58 | """Run migrations in 'online' mode.
59 |
60 | In this scenario we need to create an Engine
61 | and associate a connection with the context.
62 |
63 | """
64 |
65 | # this callback is used to prevent an auto-migration from being generated
66 | # when there are no changes to the schema
67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
68 | def process_revision_directives(context, revision, directives):
69 | if getattr(config.cmd_opts, "autogenerate", False):
70 | script = directives[0]
71 | if script.upgrade_ops.is_empty():
72 | directives[:] = []
73 | logger.info("No changes in schema detected.")
74 |
75 | connectable = engine_from_config(
76 | config.get_section(config.config_ini_section),
77 | prefix="sqlalchemy.",
78 | poolclass=pool.NullPool,
79 | )
80 |
81 | with connectable.connect() as connection:
82 | context.configure(
83 | connection=connection,
84 | target_metadata=target_metadata,
85 | process_revision_directives=process_revision_directives,
86 | **current_app.extensions["migrate"].configure_args
87 | )
88 |
89 | with context.begin_transaction():
90 | context.run_migrations()
91 |
92 |
93 | if context.is_offline_mode():
94 | run_migrations_offline()
95 | else:
96 | run_migrations_online()
97 |
--------------------------------------------------------------------------------
/app/templates/info_application.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 |
4 |
5 |
6 | Application "{{application.nom_application}}" -
7 | code "{{application.code_application}}"
8 |
9 |
{{application.desc_application}}
10 |
11 |
12 |
13 |
23 |
24 |
25 | {% for profil in profils %}
26 | {{profil.profil_rel.nom_profil}} -
27 | {% endfor %}
28 |
29 |
30 |
31 |
32 |
61 |
62 |
63 |
64 | Modifier l'application
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1.2
2 |
3 | ARG DEPS=build
4 |
5 | FROM python:3.9-bullseye AS build
6 |
7 | ENV PIP_ROOT_USER_ACTION=ignore
8 | RUN --mount=type=cache,target=/root/.cache \
9 | pip install --upgrade pip setuptools wheel
10 |
11 |
12 | FROM build AS build-usershub-auth-module
13 |
14 | WORKDIR /build/
15 | COPY /dependencies/UsersHub-authentification-module .
16 | RUN python setup.py bdist_wheel
17 |
18 |
19 | FROM build AS build-usershub
20 |
21 | WORKDIR /build/
22 | COPY /setup.py .
23 | COPY /requirements-common.in .
24 | COPY /requirements-dependencies.in .
25 | COPY /VERSION .
26 | COPY /MANIFEST.in .
27 | COPY /README.rst .
28 | COPY /LICENSE .
29 | COPY /app ./app
30 | COPY /config/docker_config.py ./app/config.py
31 | RUN python setup.py bdist_wheel
32 |
33 |
34 | FROM node:alpine AS node
35 |
36 | WORKDIR /dist/
37 | COPY /app/static/package*.json .
38 | RUN --mount=type=cache,target=/root/.npm \
39 | npm ci --omit=dev
40 |
41 |
42 | FROM python:3.9-bullseye AS dev
43 |
44 | WORKDIR /dist/
45 |
46 | RUN python -m venv /dist/venv
47 | ENV PATH="/dist/venv/bin:$PATH"
48 |
49 | RUN --mount=type=cache,target=/root/.cache \
50 | pip install --upgrade pip setuptools wheel
51 |
52 | COPY /setup.py .
53 | COPY /requirements-common.in .
54 | COPY /requirements-dependencies.in .
55 | COPY /VERSION .
56 | COPY /MANIFEST.in .
57 | COPY /README.rst .
58 | COPY /LICENSE .
59 | COPY /dependencies/ ./dependencies/
60 | COPY /requirements-dev.txt .
61 | RUN --mount=type=cache,target=/root/.cache \
62 | pip install -e . -r requirements-dev.txt
63 |
64 | COPY ./config/docker_config.py ./config.py
65 | ENV USERSHUB_SETTINGS=/dist/config.py
66 |
67 | ENV FLASK_APP=app.app:create_app
68 | CMD ["flask", "run", "--host", "0.0.0.0", "--port", "5001"]
69 |
70 |
71 | FROM python:3.9-bullseye AS app
72 | RUN --mount=type=cache,target=/root/.cache \
73 | pip install --upgrade pip setuptools wheel
74 |
75 | WORKDIR /dist/
76 |
77 | ENV PIP_ROOT_USER_ACTION=ignore
78 | RUN --mount=type=cache,target=/root/.cache \
79 | pip install --upgrade pip setuptools wheel
80 |
81 | COPY --from=node /dist/node_modules ./static/node_modules
82 |
83 | COPY /app/static ./static
84 |
85 | FROM app AS app-build
86 |
87 | COPY /requirements-dev.txt .
88 | RUN sed -i 's/^-e .*/# &/' requirements-dev.txt
89 | RUN --mount=type=cache,target=/root/.cache \
90 | pip install -r requirements-dev.txt
91 |
92 | COPY --from=build-usershub-auth-module /build/dist/*.whl .
93 | COPY --from=build-usershub /build/dist/*.whl .
94 | RUN --mount=type=cache,target=/root/.cache \
95 | pip install *.whl
96 |
97 |
98 | FROM app AS app-pypi
99 |
100 | COPY /requirements.txt .
101 | RUN --mount=type=cache,target=/root/.cache \
102 | pip install -r requirements.txt
103 |
104 | COPY --from=build-usershub /build/dist/*.whl .
105 | RUN --mount=type=cache,target=/root/.cache \
106 | pip install *.whl
107 |
108 |
109 | FROM app-${DEPS} AS prod
110 |
111 | ENV FLASK_APP=app.app:create_app
112 | ENV PYTHONPATH=/dist/config/
113 | ENV USERSHUB_SETTINGS=config.py
114 | ENV USERSHUB_STATIC_FOLDER=/dist/static
115 |
116 | EXPOSE 5001
117 |
118 | CMD ["gunicorn", "app.app:create_app()", "--bind=0.0.0.0:5001", "--access-logfile=-", "--error-logfile=-", "--reload", "--reload-extra-file=config/config.py"]
119 |
--------------------------------------------------------------------------------
/app/templates/info_organisme.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 |
4 | {% block content %}
5 | {%set is_fax = org['fax_organisme'] is not none and org['fax_organisme'] != '' %}
6 | {%set is_addess = org['adresse_organisme'] is not none and org['adresse_organisme'] != '' %}
7 | {%set is_cp = org['cp_organisme'] is not none and org['cp_organisme'] != '' %}
8 | {%set is_ville = org['ville_organisme'] is not none and org['ville_organisme'] != '' %}
9 | {%set is_tel = org['tel_organisme'] is not none and org['tel_organisme'] != '' %}
10 | {%set is_mail = org['email_organisme'] is not none and org['email_organisme'] != '' %}
11 | {%set is_url = org['url_organisme'] is not none and org['url_organisme'] != '' %}
12 | {%set is_logo = org['url_logo'] is not none and org['url_logo'] != '' %}
13 | {%set is_users = users|length > 0 %}
14 |
15 | {%if is_logo %}
16 |
17 | {% endif %}
18 |
Organisme "{{org['nom_organisme']}}"
19 |
20 |
21 | {%if is_addess %}
22 | {{org['adresse_organisme']}}
23 | {% endif %}
24 |
25 | {%if is_cp %}{{org['cp_organisme']}}{% endif %} {%if is_ville %}{{org['ville_organisme']}}{% endif %}
26 | {%if is_tel %}
27 | tel : {{org['tel_organisme']}}
28 | {% endif %}
29 | {%if is_fax %}
30 | fax : {{org['fax_organisme']}}
31 | {% endif %}
32 | {%if is_mail %}
33 | Email : {{org['email_organisme']}}
34 | {% endif %}
35 | {%if is_url %}
36 | Site web : {{org['url_organisme']}}
37 | {% endif %}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 | {% if is_users %}
49 |
57 | {% else %}
58 |
L'organisme ne comporte aucun utilisateur.
59 | {% endif %}
60 |
61 |
62 |
63 |
64 |
65 | Modifier l'organisme
66 |
67 |
68 |
69 | {% endblock %}
70 |
--------------------------------------------------------------------------------
/app/templates/app_profils.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 |
4 |
5 |
{{info}}
6 |
7 |
8 |
9 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/docs/FAQ.rst:
--------------------------------------------------------------------------------
1 | FAQ
2 | ===
3 |
4 | Comment utiliser UsersHub ?
5 | ---------------------------
6 |
7 | - Commencer par créer des organismes et des groupes
8 | - Puis créer des utilisateurs
9 | - Les ajouter à des groupes
10 | - Eventuellement ajouter des groupes (ou utilisateurs) dans les différentes listes
11 | - Définir les profils disponibles pour chaque application. Créer de nouveaux profils si nécessaire
12 | - Associer des profils à des groupes dans chaque application
13 |
14 | Il est conseillé de privilégier l'association de listes et profils à des groupes plutôt qu'à des utilisateurs.
15 |
16 | .. image :: http://geonature.fr/img/uhv2-screenshot.jpg
17 |
18 | Quelles sont les applications compatibles et leurs profils disponibles ?
19 | ------------------------------------------------------------------------
20 |
21 | **UsersHub** :
22 |
23 | - Référent (3) = Gestion des utilisateurs de son organisme uniquement (non implémenté actuellement)
24 | - Administrateur (6) = Tous les droits
25 |
26 | **TaxHub** :
27 |
28 | - Rédacteur (2) = Gestion des médias uniquement
29 | - Référent (3) = Idem 2 + Gestion des attributs de GeoNature-atlas
30 | - Modérateur (4) = Idem 3 + Possibilité d'ajouter des taxons dans bib_noms, de les mettre dans des listes et de renseigner tous leurs attributs (notamment ceux utilisés par GeoNature)
31 | - Administrateur (6) = Tous les droits
32 |
33 | **GeoNature V1** :
34 |
35 | - 2 = Rédacteurs qui peuvent saisir dans tous les protocoles, modifier leurs propres données et exporter les données de leur organisme
36 | - 6 = Administrateurs qui peuvent modifier toutes les données
37 |
38 | **GeoNature V2** :
39 |
40 | - Lecteur (1) = Permet de donner accès à un groupe ou utilisateur à GeoNature. Les permissions applicatives sont ensuite gérées au niveau de GeoNature (CRUVED)
41 |
42 | **Geotrek** (https://github.com/GeotrekCE/Geotrek-admin) :
43 |
44 | Nécessite d'activer l'authentification externe (https://geotrek.readthedocs.io/en/master/advanced-configuration.html#external-authent) et de créer une vue dans la BDD de UsersHub qui renvoie les informations à plat comme indiqué dans la documentation de Geotrek (https://github.com/PnX-SI/Ressources-techniques/blob/master/Geotrek/droits-usershub.sql).
45 |
46 | - 1 = Lecture et export dans tous les modules
47 | - 2 = Rédacteur (création, modification, suppression) dans les modules Sentiers, Statuts, Aménagements, Signalétique, Interventions et Chantiers) + Lecture et export dans tous les modules
48 | - 3 = Référent Sentiers pouvant en plus créer, modifier et supprimer dans le module Tronçons. Accès à l'AdminSite pour gérer les valeurs des listes déroulantes des modules de gestion.
49 | - 4 = Référent Communication pouvant lire et exporter dans tous les modules. Création, modification et suppression dans les modules Itinéraires, POIs, Contenus et Evenements touristiques, Services. Accès à l'AdminSite pour gérer les valeurs des listes déroulantes des modules de gestion.
50 | - 6 = Administrateurs pouvant créer, modifier et supprimer dans tous les modules.
51 |
52 | Les autorisations relatives à chaque niveau de droit sont modifiables, groupe par groupe et objet par objet dans l'AdminSite de Geotrek.
53 |
54 | **Police** (https://github.com/PnEcrins/Police) :
55 |
56 | - 1 = Lecture uniquement
57 | - 2 = Rédacteurs pouvant créer des interventions et modifier leurs interventions
58 | - 3 = Référents pouvant modifier, supprimer ou exporter toutes les interventions et renseigner les informations sur les suites données aux interventions
59 | - 6 = Administrateurs. Idem au niveau 3.
60 |
61 | **PatBati** (https://github.com/PnEcrins/PatBati) :
62 |
63 | - 1 = Lecture
64 | - 6 = Création, modification, suppression
65 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.9
3 | # by the following command:
4 | #
5 | # pip-compile requirements.in
6 | #
7 | alembic==1.14.1
8 | # via
9 | # flask-migrate
10 | # pypnusershub
11 | authlib==1.5.1
12 | # via pypnusershub
13 | bcrypt==4.3.0
14 | # via pypnusershub
15 | blinker==1.9.0
16 | # via flask
17 | certifi==2025.1.31
18 | # via requests
19 | cffi==1.17.1
20 | # via cryptography
21 | charset-normalizer==3.4.1
22 | # via requests
23 | click==8.1.8
24 | # via flask
25 | cryptography==43.0.3
26 | # via authlib
27 | dnspython==2.7.0
28 | # via email-validator
29 | email-validator==2.2.0
30 | # via -r requirements-common.in
31 | flask==3.1.0
32 | # via
33 | # -r requirements-common.in
34 | # flask-login
35 | # flask-marshmallow
36 | # flask-migrate
37 | # flask-sqlalchemy
38 | # flask-wtf
39 | # pypnusershub
40 | # utils-flask-sqlalchemy
41 | flask-login==0.6.3
42 | # via pypnusershub
43 | flask-marshmallow==1.3.0
44 | # via pypnusershub
45 | flask-migrate==4.1.0
46 | # via
47 | # -r requirements-common.in
48 | # utils-flask-sqlalchemy
49 | flask-sqlalchemy==3.0.5
50 | # via
51 | # -r requirements-common.in
52 | # flask-migrate
53 | # pypnusershub
54 | # utils-flask-sqlalchemy
55 | flask-wtf==1.2.2
56 | # via -r requirements-common.in
57 | greenlet==3.1.1
58 | # via sqlalchemy
59 | gunicorn==23.0.0
60 | # via -r requirements-common.in
61 | idna==3.10
62 | # via
63 | # email-validator
64 | # requests
65 | importlib-metadata==8.6.1
66 | # via flask
67 | infinity==1.5
68 | # via intervals
69 | intervals==0.9.2
70 | # via wtforms-components
71 | itsdangerous==2.2.0
72 | # via
73 | # flask
74 | # flask-wtf
75 | jinja2==3.1.5
76 | # via flask
77 | mako==1.3.9
78 | # via alembic
79 | markupsafe==3.0.2
80 | # via
81 | # jinja2
82 | # mako
83 | # werkzeug
84 | # wtforms
85 | # wtforms-components
86 | marshmallow==3.26.1
87 | # via
88 | # flask-marshmallow
89 | # marshmallow-sqlalchemy
90 | # utils-flask-sqlalchemy
91 | marshmallow-sqlalchemy==1.4.1
92 | # via
93 | # -r requirements-common.in
94 | # pypnusershub
95 | packaging==24.2
96 | # via
97 | # gunicorn
98 | # marshmallow
99 | psycopg2==2.9.10
100 | # via
101 | # -r requirements-common.in
102 | # pypnusershub
103 | pycparser==2.22
104 | # via cffi
105 | pypnusershub==3.0.3
106 | # via -r requirements-dependencies.in
107 | python-dateutil==2.9.0.post0
108 | # via
109 | # -r requirements-common.in
110 | # utils-flask-sqlalchemy
111 | python-dotenv==1.0.1
112 | # via -r requirements-common.in
113 | requests==2.32.3
114 | # via pypnusershub
115 | six==1.17.0
116 | # via python-dateutil
117 | sqlalchemy==1.4.54
118 | # via
119 | # alembic
120 | # flask-sqlalchemy
121 | # marshmallow-sqlalchemy
122 | # pypnusershub
123 | # utils-flask-sqlalchemy
124 | typing-extensions==4.12.2
125 | # via
126 | # alembic
127 | # marshmallow-sqlalchemy
128 | urllib3==2.3.0
129 | # via requests
130 | utils-flask-sqlalchemy==0.4.1
131 | # via pypnusershub
132 | validators==0.34.0
133 | # via wtforms-components
134 | werkzeug==3.1.3
135 | # via
136 | # flask
137 | # flask-login
138 | wtforms==3.2.1
139 | # via
140 | # -r requirements-common.in
141 | # flask-wtf
142 | # wtforms-components
143 | wtforms-components==0.11.0
144 | # via -r requirements-common.in
145 | xmltodict==0.14.2
146 | # via pypnusershub
147 | zipp==3.21.0
148 | # via importlib-metadata
149 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.9
3 | # by the following command:
4 | #
5 | # pip-compile requirements-dev.in
6 | #
7 | -e file:dependencies/UsersHub-authentification-module#egg=pypnusershub
8 | # via -r requirements-submodules.in
9 | alembic==1.14.1
10 | # via
11 | # flask-migrate
12 | # pypnusershub
13 | authlib==1.5.1
14 | # via pypnusershub
15 | bcrypt==4.3.0
16 | # via pypnusershub
17 | blinker==1.9.0
18 | # via flask
19 | certifi==2025.1.31
20 | # via requests
21 | cffi==1.17.1
22 | # via cryptography
23 | charset-normalizer==3.4.1
24 | # via requests
25 | click==8.1.8
26 | # via flask
27 | cryptography==43.0.3
28 | # via authlib
29 | dnspython==2.7.0
30 | # via email-validator
31 | email-validator==2.2.0
32 | # via -r requirements-common.in
33 | flask==3.1.0
34 | # via
35 | # -r requirements-common.in
36 | # flask-login
37 | # flask-marshmallow
38 | # flask-migrate
39 | # flask-sqlalchemy
40 | # flask-wtf
41 | # pypnusershub
42 | # utils-flask-sqlalchemy
43 | flask-login==0.6.3
44 | # via pypnusershub
45 | flask-marshmallow==1.3.0
46 | # via pypnusershub
47 | flask-migrate==4.1.0
48 | # via
49 | # -r requirements-common.in
50 | # utils-flask-sqlalchemy
51 | flask-sqlalchemy==3.0.5
52 | # via
53 | # -r requirements-common.in
54 | # flask-migrate
55 | # pypnusershub
56 | # utils-flask-sqlalchemy
57 | flask-wtf==1.2.2
58 | # via -r requirements-common.in
59 | greenlet==3.1.1
60 | # via sqlalchemy
61 | gunicorn==23.0.0
62 | # via -r requirements-common.in
63 | idna==3.10
64 | # via
65 | # email-validator
66 | # requests
67 | importlib-metadata==8.6.1
68 | # via flask
69 | infinity==1.5
70 | # via intervals
71 | intervals==0.9.2
72 | # via wtforms-components
73 | itsdangerous==2.2.0
74 | # via
75 | # flask
76 | # flask-wtf
77 | jinja2==3.1.5
78 | # via flask
79 | mako==1.3.9
80 | # via alembic
81 | markupsafe==3.0.2
82 | # via
83 | # jinja2
84 | # mako
85 | # werkzeug
86 | # wtforms
87 | # wtforms-components
88 | marshmallow==3.26.1
89 | # via
90 | # flask-marshmallow
91 | # marshmallow-sqlalchemy
92 | # utils-flask-sqlalchemy
93 | marshmallow-sqlalchemy==1.4.1
94 | # via
95 | # -r requirements-common.in
96 | # pypnusershub
97 | packaging==24.2
98 | # via
99 | # gunicorn
100 | # marshmallow
101 | psycopg2==2.9.10
102 | # via
103 | # -r requirements-common.in
104 | # pypnusershub
105 | pycparser==2.22
106 | # via cffi
107 | python-dateutil==2.9.0.post0
108 | # via
109 | # -r requirements-common.in
110 | # utils-flask-sqlalchemy
111 | python-dotenv==1.0.1
112 | # via -r requirements-common.in
113 | requests==2.32.3
114 | # via pypnusershub
115 | six==1.17.0
116 | # via python-dateutil
117 | sqlalchemy==1.4.54
118 | # via
119 | # alembic
120 | # flask-sqlalchemy
121 | # marshmallow-sqlalchemy
122 | # pypnusershub
123 | # utils-flask-sqlalchemy
124 | typing-extensions==4.12.2
125 | # via
126 | # alembic
127 | # marshmallow-sqlalchemy
128 | urllib3==2.3.0
129 | # via requests
130 | utils-flask-sqlalchemy==0.4.1
131 | # via pypnusershub
132 | validators==0.34.0
133 | # via wtforms-components
134 | werkzeug==3.1.3
135 | # via
136 | # flask
137 | # flask-login
138 | wtforms==3.2.1
139 | # via
140 | # -r requirements-common.in
141 | # flask-wtf
142 | # wtforms-components
143 | wtforms-components==0.11.0
144 | # via -r requirements-common.in
145 | xmltodict==0.14.2
146 | # via pypnusershub
147 | zipp==3.21.0
148 | # via importlib-metadata
149 |
--------------------------------------------------------------------------------
/app/temp_users/routes.py:
--------------------------------------------------------------------------------
1 | import requests as req_lib
2 | import logging
3 |
4 | from flask import (
5 | Blueprint,
6 | request,
7 | render_template,
8 | current_app,
9 | redirect,
10 | url_for,
11 | flash,
12 | current_app,
13 | )
14 | from pypnusershub.db.models_register import TempUser
15 | from pypnusershub import routes as fnauth
16 |
17 | from app.env import db
18 | from app.utils.utilssqlalchemy import json_resp
19 | from app.models import TApplications
20 |
21 |
22 | routes = Blueprint("temp_users", __name__)
23 | log = logging.getLogger()
24 |
25 |
26 | @routes.route("/list", methods=["GET"])
27 | @fnauth.check_auth(6)
28 | def temp_users_list():
29 | """
30 | Get all temp_users
31 | """
32 | data = db.session.query(TempUser).order_by("identifiant").all()
33 | temp_users = []
34 | for d in data:
35 | temp_user = d.as_dict()
36 | temp_user["full_name"] = f"{temp_user['nom_role']} {temp_user['prenom_role']}"
37 | app = db.session.query(TApplications).get(temp_user["id_application"])
38 | temp_user["app_name"] = None
39 | if app:
40 | temp_user["app_name"] = app.nom_application
41 | temp_users.append(temp_user)
42 | columns = [
43 | {"key": "identifiant", "label": "Login"},
44 | {"key": "full_name", "label": "Nom - Prénom"},
45 | {"key": "email", "label": "Email"},
46 | {"key": "app_name", "label": "Application"},
47 | {"key": "champs_addi", "label": "Autres"},
48 | ]
49 | return render_template("temp_users.html", data=temp_users, columns=columns)
50 |
51 |
52 | @routes.route("/validate//", methods=["GET"])
53 | @fnauth.check_auth(6)
54 | def validate(token, id_application):
55 | """
56 | Call the API to validate a temp user
57 | """
58 | data_to_post = {"token": token, "id_application": id_application}
59 |
60 | # Get temp user infos
61 | temp_user = (
62 | db.session.query(TempUser.confirmation_url)
63 | .filter(token == TempUser.token_role)
64 | .first()
65 | )
66 | if not temp_user:
67 | return {"msg": "Aucun utilisateur trouvé avec le token user demandé"}, 404
68 | url_after_validation = temp_user[0]
69 |
70 | # Call temp user validation URL
71 | url_validate = (
72 | current_app.config["URL_APPLICATION"] + "/api_register/valid_temp_user"
73 | )
74 | r = req_lib.post(url=url_validate, json=data_to_post, cookies=request.cookies)
75 | if r.status_code != 200:
76 | flash("Erreur durant la validation de l'utilisateur temporaire", "error")
77 | return redirect(url_for("temp_users.temp_users_list"))
78 | elif not url_after_validation:
79 | flash("L'utilisateur a bien été validé")
80 | return redirect(url_for("temp_users.temp_users_list"))
81 |
82 | user_data = r.json()
83 |
84 | # Call post UsersHub actions URL
85 | if url_after_validation:
86 | r = req_lib.post(
87 | url=url_after_validation, json=user_data, cookies=request.cookies
88 | )
89 | if r.status_code == 200:
90 | flash("L'utilisateur a bien été validé")
91 | return redirect(url_for("temp_users.temp_users_list"))
92 | else:
93 | flash("Erreur durant l'appel des actions de l'application", "error")
94 | log.error("Error HTTP {} for {}".format(r.status_code, url_after_validation))
95 | return redirect(url_for("temp_users.temp_users_list"))
96 |
97 |
98 | @routes.route("/delete/", methods=["GET"])
99 | @fnauth.check_auth(6)
100 | def delete(token):
101 | """
102 | DELETE a temp_user
103 | """
104 |
105 | temp_user = db.session.query(TempUser).filter(TempUser.token_role == token).first()
106 | if temp_user:
107 | db.session.delete(temp_user)
108 | db.session.commit()
109 | flash("L'utilisateur a bien été supprimé")
110 | return redirect(url_for("temp_users.temp_users_list"))
111 | else:
112 | flash("Une erreur s'est produite", "error")
113 | return redirect(url_for("temp_users.temp_users_list"))
114 |
--------------------------------------------------------------------------------
/app/genericRepository.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.sql import text
2 | from app.env import db
3 |
4 |
5 | class GenericRepository(db.Model):
6 | __abstract__ = True
7 |
8 | """
9 | Classe abstraite contenant des méthodes générique d'ajout/suppression/lecture/mise à jour de la base
10 | """
11 |
12 | @classmethod
13 | def get_one(cls, id, as_model=False):
14 | """
15 | Methode qui retourne un dictionnaire d'un élément d'un Model
16 | Avec pour paramètres l'id de l'élément
17 | Si as_model != False alors au lieu de retourner un dictionnaire on retourne l'object du modèle
18 | """
19 | if not id:
20 | return None
21 |
22 | data = db.session.query(cls).get(id)
23 |
24 | if not as_model:
25 | return data.as_dict(True)
26 | else:
27 | return data
28 |
29 | @classmethod
30 | def get_all(
31 | cls,
32 | columns=None,
33 | params=None,
34 | recursif=True,
35 | as_model=False,
36 | order_by=None,
37 | order="asc",
38 | ):
39 | """
40 | Methode qui retourne un dictionnaire de tout les éléments d'un Model
41 | Avec pour paramètres:
42 | columns un tableau des colonnes que l'ont souhaite récupérer
43 | params un tableau contenant un dictionnaire de filtre [{'col': colonne à filtrer, 'filter': paramètre de filtrage}]
44 | si recursif != True on désactive la fonction récursive du as_dict()
45 | si as_model != False alors au lieu de retourner un dictionnaire on retourne une requête
46 | Si as_model != False alors au lieu de retourner un dictionnaire on retourne un tableau d'objets du modèle
47 | """
48 |
49 | data = None
50 | q = db.session.query(cls)
51 | if params:
52 | q = db.session.query(cls)
53 | for param in params:
54 | nom_col = getattr(cls, param["col"])
55 | q = q.filter(nom_col == param["filter"])
56 | if order_by:
57 | order_col = getattr(cls, order_by)
58 | order_col = order_col.desc() if order == "desc" else order_col.asc()
59 | q = q.order_by(order_col)
60 | data = q.all()
61 | if as_model:
62 | return data
63 | return [d.as_dict(recursif, columns) for d in data]
64 |
65 | @classmethod
66 | def post(cls, entity_dict):
67 | """
68 | Methode qui ajoute un élément à une table
69 | Avec pour paramètres un dictionnaire de cet élément
70 | Retourne le modèle nouvellement ajouté
71 | """
72 | try:
73 | model = cls(**entity_dict)
74 | db.session.add(model)
75 | db.session.commit()
76 | return model
77 |
78 | except Exception:
79 | db.session.rollback()
80 | raise
81 |
82 | @classmethod
83 | def update(cls, entity_dict):
84 | """
85 | Methode qui met à jour un élément
86 | Avec pour paramètre un dictionnaire de cet élément
87 | Retourne le modèle mis à jour
88 | """
89 | try:
90 | model = cls(**entity_dict)
91 | db.session.merge(model)
92 | db.session.commit()
93 | return model
94 | except Exception as e:
95 | print(e)
96 | db.session.rollback()
97 | raise
98 |
99 | @classmethod
100 | def delete(cls, id):
101 | """
102 | Methode qui supprime un élement d'une table à partir d'un id donné
103 | Avec pour paramètre un id (clé primaire)
104 | """
105 | try:
106 | db.session.delete(db.session.query(cls).get(id))
107 | db.session.commit()
108 | except Exception:
109 | db.session.rollback()
110 | raise
111 |
112 | @classmethod
113 | def choixSelect(cls, id, nom, aucun=None, order_by=None):
114 | """
115 | Methode qui retourne un tableau de tuples d'id et de nom
116 | Avec pour paramètres un id et un nom
117 | Le paramètre aucun si il a une valeur permet de rajouter le tuple (-1,Aucun) au tableau
118 | """
119 |
120 | data = cls.get_all(order_by=order_by)
121 | choices = []
122 | for d in data:
123 | choices.append((d[id], d[nom]))
124 | if aucun != None:
125 | choices.append((-1, "Aucun"))
126 | return choices
127 |
128 | # def get_column_name(cls,columns=None):
129 | # if columns:
130 | # for col in cls.__table__.columns.keys()
131 |
132 | # return cls.__table__.columns.keys()
133 |
--------------------------------------------------------------------------------
/app/app.py:
--------------------------------------------------------------------------------
1 | """
2 | Serveur de l'application UsersHub
3 | """
4 |
5 | import os
6 | import sys
7 | import json
8 | import logging
9 | from pkg_resources import iter_entry_points
10 | from urllib.parse import urlsplit, urlencode
11 | from pathlib import Path
12 |
13 | from flask import (
14 | Flask,
15 | Response,
16 | redirect,
17 | url_for,
18 | request,
19 | session,
20 | render_template,
21 | g,
22 | )
23 | from werkzeug.middleware.proxy_fix import ProxyFix
24 | from sqlalchemy.exc import ProgrammingError
25 | from flask_migrate import Migrate
26 |
27 | from app.env import db
28 |
29 | from pypnusershub.db.models import Application
30 | from app.utils.errors import handle_unauthenticated_request
31 | from pypnusershub.auth import auth_manager
32 | import importlib.metadata
33 |
34 | migrate = Migrate()
35 |
36 |
37 | @migrate.configure
38 | def configure_alembic(alembic_config):
39 | """
40 | This function add to the 'version_locations' parameter of the alembic config the
41 | 'migrations' entry point value of the 'gn_module' group for all modules having such entry point.
42 | Thus, alembic will find migrations of all installed geonature modules.
43 | """
44 | version_locations = alembic_config.get_main_option(
45 | "version_locations", default=""
46 | ).split()
47 | for entry_point in iter_entry_points("alembic", "migrations"):
48 | _, migrations = str(entry_point).split("=", 1)
49 | version_locations += [migrations.strip()]
50 | alembic_config.set_main_option("version_locations", " ".join(version_locations))
51 | return alembic_config
52 |
53 |
54 | def create_app():
55 | app = Flask(
56 | __name__, static_folder=os.environ.get("USERSHUB_STATIC_FOLDER", "static")
57 | )
58 | app.config.from_pyfile(os.environ.get("USERSHUB_SETTINGS", "../config/config.py"))
59 | app.config.from_prefixed_env(prefix="USERSHUB")
60 | app.config["APPLICATION_ROOT"] = urlsplit(app.config["URL_APPLICATION"]).path or "/"
61 | if "SCRIPT_NAME" not in os.environ and app.config["APPLICATION_ROOT"] != "/":
62 | os.environ["SCRIPT_NAME"] = app.config["APPLICATION_ROOT"]
63 | app.config["URL_REDIRECT"] = "{}/{}".format(app.config["URL_APPLICATION"], "login")
64 | app.secret_key = app.config["SECRET_KEY"]
65 | app.config["VERSION"] = importlib.metadata.version("usershub")
66 | app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1)
67 | db.init_app(app)
68 | app.config["DB"] = db
69 | providers_config = [
70 | {
71 | "module": "pypnusershub.auth.providers.default.LocalProvider",
72 | "id_provider": "local_provider",
73 | },
74 | ]
75 | auth_manager.init_app(
76 | app, prefix="/pypn/auth/", providers_declaration=providers_config
77 | )
78 | auth_manager.home_page = app.config["URL_APPLICATION"]
79 |
80 | migrate.init_app(app, db, directory=Path(__file__).absolute().parent / "migrations")
81 |
82 | if "CODE_APPLICATION" not in app.config:
83 | app.config["CODE_APPLICATION"] = "UH"
84 |
85 | with app.app_context():
86 | app.jinja_env.globals["url_application"] = app.config["URL_APPLICATION"]
87 |
88 | if app.config["ACTIVATE_APP"]:
89 |
90 | @app.route("/")
91 | def index():
92 | """Route par défaut de l'application"""
93 | return redirect(url_for("user.users"))
94 |
95 | @app.route("/constants.js")
96 | def constants_js():
97 | """Route des constantes javascript"""
98 | return render_template("constants.js")
99 |
100 | @app.context_processor
101 | def inject_user():
102 | return dict(user=getattr(g, "user", None))
103 |
104 | from app.t_roles import route
105 |
106 | app.register_blueprint(route.route, url_prefix="/")
107 |
108 | from app.bib_organismes import route
109 |
110 | app.register_blueprint(route.route, url_prefix="/")
111 |
112 | from app.groupe import route
113 |
114 | app.register_blueprint(route.route, url_prefix="/")
115 |
116 | from app.liste import route
117 |
118 | app.register_blueprint(route.route, url_prefix="/")
119 |
120 | from app.t_applications import route
121 |
122 | app.register_blueprint(route.route, url_prefix="/")
123 |
124 | from app.t_profils import route
125 |
126 | app.register_blueprint(route.route, url_prefix="/")
127 |
128 | from app.login import route
129 |
130 | app.register_blueprint(route.route, url_prefix="/")
131 |
132 | from app.temp_users import routes
133 |
134 | app.register_blueprint(routes.routes, url_prefix="/temp_users")
135 |
136 | from app.api import route
137 |
138 | app.register_blueprint(route.route, url_prefix="/api")
139 |
140 | if app.config["ACTIVATE_API"]:
141 | from app.api import route_register
142 |
143 | app.register_blueprint(
144 | route_register.route, url_prefix="/api_register"
145 | ) # noqa
146 |
147 | app.login_manager.unauthorized_handler(handle_unauthenticated_request)
148 |
149 | return app
150 |
--------------------------------------------------------------------------------
/app/templates/tobelong.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% include "librairies.html" %}
4 |
5 | {% include "head-appli.html" %}
6 |
7 |
8 |
9 |
{{info}}
10 |
11 |
12 |
13 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/app/t_profils/route.py:
--------------------------------------------------------------------------------
1 | from flask import (
2 | redirect,
3 | url_for,
4 | render_template,
5 | Blueprint,
6 | request,
7 | flash,
8 | current_app,
9 | )
10 |
11 | from pypnusershub import routes as fnauth
12 |
13 | from app.t_profils import forms as t_profilsforms
14 | from app.models import (
15 | TProfils,
16 | TApplications,
17 | CorProfilForApp,
18 | TRoles,
19 | Bib_Organismes,
20 | CorRoleAppProfil,
21 | )
22 |
23 | URL_REDIRECT = current_app.config["URL_REDIRECT"]
24 | URL_APPLICATION = current_app.config["URL_APPLICATION"]
25 |
26 | route = Blueprint("profils", __name__)
27 |
28 | """
29 | Routes des profils
30 | """
31 |
32 |
33 | @route.route("profils/list", methods=["GET", "POST"])
34 | @fnauth.check_auth(
35 | 3,
36 | )
37 | def profils():
38 | """
39 | Route qui affiche la liste des profils
40 | Retourne un template avec pour paramètres :
41 | - une entête de tableau --> fLine
42 | - le nom des colonnes de la base --> line
43 | - le contenu du tableau --> table
44 | - le chemin de mise à jour --> pathU
45 | - le chemin de suppression --> pathD
46 | - le chemin d'ajout --> pathA
47 | - le chemin des roles du profil --> pathP
48 | - une clé (clé primaire dans la plupart des cas) --> key
49 | - un nom (nom de la table) pour le bouton ajout --> name
50 | - un nom de listes --> name_list
51 | - ajoute une colonne de bouton ('True' doit être de type string)--> otherCol
52 | - nom affiché sur le bouton --> Members
53 | """
54 |
55 | fLine = ["ID", "CODE", "Nom", "Description"]
56 | columns = ["id_profil", "code_profil", "nom_profil", "desc_profil"]
57 | tab = [data for data in TProfils.get_all(order_by="nom_profil")]
58 | return render_template(
59 | "table_database.html",
60 | fLine=fLine,
61 | line=columns,
62 | table=tab,
63 | key="id_profil",
64 | pathU=URL_APPLICATION + "/profil/update/",
65 | pathD=URL_APPLICATION + "/profil/delete/",
66 | pathA=URL_APPLICATION + "/profil/add/new",
67 | name="un profil",
68 | name_list="Profils",
69 | otherCol="False",
70 | profil_app="True",
71 | App="Application",
72 | )
73 |
74 |
75 | @route.route("profil/delete/", methods=["GET", "POST"])
76 | @fnauth.check_auth(
77 | 6,
78 | )
79 | def delete(id_profil):
80 | """
81 | Route qui supprime un profil dont l'id est donné en paramètres dans l'url
82 | Retourne une redirection vers la liste de profil
83 | """
84 |
85 | TProfils.delete(id_profil)
86 | return redirect(url_for("profils.profils"))
87 |
88 |
89 | @route.route("profil/add/new", defaults={"id_profil": None}, methods=["GET", "POST"])
90 | @route.route("profil/update/", methods=["GET", "POST"])
91 | @fnauth.check_auth(
92 | 6,
93 | )
94 | def addorupdate(id_profil):
95 | """
96 | Route affichant un formulaire vierge ou non (selon l'url) pour ajouter ou mettre à jour un profil
97 | L'envoie du formulaire permet l'ajout ou la maj du profil dans la base
98 | Retourne un template accompagné d'un formulaire pré-rempli ou non selon le paramètre id_profil
99 | Une fois le formulaire validé on retourne une redirection vers la liste de profil
100 | """
101 |
102 | form = t_profilsforms.Profil()
103 | if id_profil == None:
104 | if request.method == "POST":
105 | if form.validate() and form.validate_on_submit():
106 | form_profil = pops(form.data)
107 | form_profil.pop("id_profil")
108 | TProfils.post(form_profil)
109 | return redirect(url_for("profils.profils"))
110 | return render_template("profil.html", form=form, title="Formulaire Profil")
111 | else:
112 | profil = TProfils.get_one(id_profil)
113 | if request.method == "GET":
114 | form = process(form, profil)
115 | if request.method == "POST":
116 | if form.validate() and form.validate_on_submit():
117 | form_profil = pops(form.data)
118 | form_profil["id_profil"] = profil["id_profil"]
119 | TProfils.update(form_profil)
120 | return redirect(url_for("profils.profils"))
121 | return render_template("profil.html", form=form, title="Formulaire Profil")
122 |
123 |
124 | def pops(form):
125 | """
126 | Methode qui supprime les éléments indésirables du formulaires
127 | Avec pour paramètre un formulaire
128 | """
129 |
130 | form.pop("csrf_token")
131 | form.pop("submit")
132 | return form
133 |
134 |
135 | def process(form, profil):
136 | """
137 | Methode qui rempli le formulaire par les données de l'éléments concerné
138 | Avec pour paramètres un formulaire et un profil
139 | """
140 |
141 | form.nom_profil.process_data(profil["nom_profil"])
142 | form.code_profil.process_data(profil["code_profil"])
143 | form.desc_profil.process_data(profil["desc_profil"])
144 | return form
145 |
--------------------------------------------------------------------------------
/docs/docker.rst:
--------------------------------------------------------------------------------
1 | ======
2 | Docker
3 | ======
4 |
5 | Image Docker de UsersHub
6 | ========================
7 |
8 | L’image Docker de UsersHub vous permet de lancer UsersHub et de le connecter à votre serveur PostgreSQL existant.
9 | Si vous n’avez pas de serveur PostgreSQL, ou que vous ne souhaitez pas l’utiliser, utilisez plutôt `Docker Compose`_.
10 |
11 | Prérequis
12 | ---------
13 |
14 | * Avoir un serveur PostgreSQL fonctionnel, avec une base de données crées dédiée à UsersHub, et des identifiants associés.
15 | * Installer `Docker Engine `_
16 |
17 | Utilisation
18 | -----------
19 |
20 | * Récupérer l’image :
21 |
22 | .. code-block:: bash
23 |
24 | docker pull ghcr.io/pnx-si/usershub:latest
25 |
26 | * Créer un conteneur à partir de l’image UsersHub et le démarrer :
27 |
28 | .. code-block:: bash
29 |
30 | docker run \
31 | -d \
32 | --name usershub \
33 | -e USERSHUB_SQLALCHEMY_DATABASE_URI="postgresql://username:password@localhost/dbname" \
34 | --network=host \
35 | ghcr.io/pnx-si/usershub:latest
36 |
37 | * Vérifier que l’image est bien lancé en accédant aux logs :
38 |
39 | .. code-block:: bash
40 |
41 | docker logs usershub -f
42 |
43 | * Créer le schéma de base de données :
44 |
45 | .. code-block:: bash
46 |
47 | docker exec -it usershub flask db exec 'create extension if not exists "uuid-ossp"'
48 | docker exec -it usershub flask db upgrade usershub@head
49 | docker exec -it usershub flask db upgrade usershub-samples@head # utilisateur admin / admin
50 |
51 |
52 | Vous pouvez à présent vous rendre sur `http://localhost:5001`_ et vous connecter avec le couple identifiant / mot de passe ``admin`` / ``admin``.
53 |
54 | Autres opérations utiles :
55 |
56 | * Stopper UsersHub : ``docker stop usershub``
57 | * Démarrer UsersHub : ``docker start usershub``
58 | * Redémarrer UsersHub : ``docker restart usershub``
59 | * Supprimer le conteneur UsersHub : ``docker rm usershub``. Le conteneur doit préalablement avoir été stoppé, et il peut être re-créé avec la commande ``docker run``.
60 |
61 | Configuration
62 | -------------
63 |
64 | * Créer un fichier ``config.py`` avec le contenu suivant :
65 |
66 | .. code-block:: python
67 |
68 | from app.config import *
69 |
70 | SECRET_KEY = "my secret key"
71 | SQLALCHEMY_DATABASE_URI = "…"
72 |
73 | * Supprimer le conteneur et le re-créer :
74 |
75 | .. code-block:: bash
76 |
77 | docker stop usershub
78 | docker rm usershub
79 | docker run \
80 | -d \
81 | --name usershub \
82 | --mount type=bind,source=$(realpath config.py),target=/dist/config.py \
83 | -e USERSHUB_SETTINGS=/dist/config.py \
84 | --network="host" \
85 | ghcr.io/pnx-si/usershub:latest
86 |
87 | * À chaque modification du fichier ``config.py``, redémarrer le conteneur :
88 |
89 | .. code-block:: bash
90 |
91 | docker restart usershub
92 |
93 | Mise à jour
94 | -----------
95 |
96 | * Récupérer la dernière version de l’image :
97 |
98 | .. code-block::bash
99 |
100 | docker pull ghcr.io/pnx-si/usershub:latest
101 |
102 | * Supprimer le conteneur :
103 |
104 | .. code-block:: bash
105 |
106 | docker stop usershub
107 | docker rm usershub
108 |
109 | * Le recréer avec la commande ``docker run`` (voir ci-dessus)
110 |
111 | * Mettre à jour la base de données :
112 |
113 | .. code-block:: bash
114 |
115 | docker exec --it usershub flask db autoupgrade
116 |
117 |
118 | Docker Compose
119 | ==============
120 |
121 | Docker Compose vous permet de lancer UsersHub et un serveur PostgreSQL dédié automatiquement.
122 |
123 | Prérequis
124 | ---------
125 |
126 | * `Installer Docker Compose `_
127 | * Récupérer et dé-archiver la dernière version du code source depuis `Github _`.
128 |
129 | Installation
130 | ------------
131 |
132 | * Ce rendre dans le dossier UsersHub : ``cd UsersHub``
133 | * Installer le schéma utilisateurs en base : ``docker compose run --rm usershub flask db upgrade usershub@head``
134 | * Installer les données d’exemple (utilisateur admin) en base : ``docker compose run --rm usershub flask db upgrade usershub-samples@head``
135 | * Lancer UsersHub : ``docker compose up -d``
136 |
137 | Vous pouvez à présent vous rendre sur ``_ et vous connecter avec le couple login / mot de passe ``admin`` / ``admin``.
138 |
139 | Autres opérations utiles :
140 |
141 | * Accéder aux logs : ``docker compose logs usershub -f``
142 | * Stopper les conteneurs : ``docker compose stop``
143 | * Redémarrer UsersHub : ``docker compose restart usershub``
144 | * Supprimer les conteneurs : ``docker compose down``
145 |
146 | Configuration
147 | -------------
148 |
149 | * Créer un fichier ``config/local_config.py`` :
150 |
151 | .. code-block:: python
152 |
153 | from app.config import *
154 |
155 | SECRET_KEY = "my secret key"
156 |
157 | * Indiquer à UsersHub d’utiliser votre fichier de configuration en créant un fichier ``.env`` contenant :
158 |
159 | .. code-block:: bash
160 |
161 | USERSHUB_SETTINGS=/dist/config/local_config.py
162 |
163 | * Lancer les containers – ils seront re-créés – afin d’utiliser votre nouveau fichier de configuration :
164 |
165 | .. code-block:: bash
166 |
167 | docker compose up -d
168 |
169 | * Au prochaines modifications du fichier ``local_config.py``, vous pouvez simplement redémarrer UsersHub :
170 |
171 | .. code-block:: bash
172 |
173 | docker compose restart usershub
174 |
175 | Mise à jour
176 | -----------
177 |
178 | .. code-block:: bash
179 |
180 | docker compose pull
181 | docker compose up -d
182 | docker compose run --rm usershub flask db autoupgrade
183 |
184 | Modification du code source
185 | ---------------------------
186 |
187 | À chaque modification du code source, vous devez :
188 |
189 | * Builder l’image : ``docker compose build``
190 | * Relancer : ``docker compose up -d``
191 |
--------------------------------------------------------------------------------
/app/bib_organismes/route.py:
--------------------------------------------------------------------------------
1 | """
2 | Route des Organismes
3 | """
4 |
5 | from flask import (
6 | Blueprint,
7 | redirect,
8 | url_for,
9 | render_template,
10 | request,
11 | flash,
12 | current_app,
13 | )
14 |
15 | from pypnusershub import routes as fnauth
16 |
17 | from app import genericRepository
18 | from app.bib_organismes import forms as bib_organismeforms
19 | from app.models import Bib_Organismes, TRoles
20 | from app.utils.utils_all import strigify_dict
21 |
22 |
23 | URL_REDIRECT = current_app.config["URL_REDIRECT"]
24 | URL_APPLICATION = current_app.config["URL_APPLICATION"]
25 | route = Blueprint("organisme", __name__)
26 |
27 |
28 | @route.route("organisms/list", methods=["GET", "POST"])
29 | @fnauth.check_auth(
30 | 3,
31 | )
32 | def organisms():
33 | """
34 | Route qui affiche la liste des Organismes
35 | Retourne un template avec pour paramètres :
36 | - une entête de tableau --> fLine
37 | - le nom des colonnes de la base --> line
38 | - le contenu du tableau --> table
39 | - le chemin de mise à jour --> pathU
40 | - le chemin de suppression --> pathD
41 | - le chemin d'ajout --> pathA
42 | - le chemin de la page d'information --> pathI
43 | - une clé (clé primaire dans la plupart des cas) --> key
44 | - un nom (nom de la table) pour le bouton ajout --> name
45 | - un nom de listes --> name_list
46 | - ajoute une colonne pour accéder aux infos de l'utilisateur --> see
47 | """
48 |
49 | fLine = [
50 | "ID",
51 | "Nom",
52 | "Adresse",
53 | "Code postal",
54 | "Ville",
55 | "Telephone",
56 | "Fax",
57 | "Email",
58 | ]
59 | columns = [
60 | "id_organisme",
61 | "nom_organisme",
62 | "adresse_organisme",
63 | "cp_organisme",
64 | "ville_organisme",
65 | "tel_organisme",
66 | "fax_organisme",
67 | "email_organisme",
68 | ]
69 | contents = Bib_Organismes.get_all(columns, order_by="nom_organisme")
70 | return render_template(
71 | "table_database.html",
72 | table=contents,
73 | fLine=fLine,
74 | line=columns,
75 | key="id_organisme",
76 | pathI=URL_APPLICATION + "/organism/info/",
77 | pathU=URL_APPLICATION + "/organism/update/",
78 | pathD=URL_APPLICATION + "/organisms/delete/",
79 | pathA=URL_APPLICATION + "/organism/add/new",
80 | name="un organisme",
81 | name_list="Organismes",
82 | see="True",
83 | )
84 |
85 |
86 | @route.route(
87 | "organism/add/new", defaults={"id_organisme": None}, methods=["GET", "POST"]
88 | )
89 | @route.route("organism/update/", methods=["GET", "POST"])
90 | @fnauth.check_auth(
91 | 6,
92 | )
93 | def addorupdate(id_organisme):
94 | """
95 | Route affichant un formulaire vierge ou non (selon l'url) pour ajouter ou mettre à jour un organisme
96 | L'envoie du formulaire permet l'ajout ou la mise à jour de l'éléments dans la base
97 | Retourne un template accompagné du formulaire
98 | Une fois le formulaire validé on retourne une redirection vers la liste d'organisme
99 | """
100 |
101 | form = bib_organismeforms.Organisme()
102 | if id_organisme == None:
103 | if request.method == "POST":
104 | if form.validate_on_submit() and form.validate():
105 | form_org = pops(form.data)
106 | form_org.pop("id_organisme")
107 | Bib_Organismes.post(form_org)
108 | return redirect(url_for("organisme.organisms"))
109 | else:
110 | flash(strigify_dict(form.errors), "error")
111 | else:
112 | org = Bib_Organismes.get_one(id_organisme)
113 | if request.method == "GET":
114 | form = bib_organismeforms.Organisme(**org)
115 | if request.method == "POST":
116 | if form.validate_on_submit() and form.validate():
117 | form_org = pops(form.data)
118 | form_org["id_organisme"] = org["id_organisme"]
119 | Bib_Organismes.update(form_org)
120 | return redirect(url_for("organisme.organisms"))
121 | else:
122 | flash(strigify_dict(form.errors), "error")
123 | return render_template("organism.html", form=form, title="Formulaire Organisme")
124 |
125 |
126 | @route.route("organisms/delete/", methods=["GET", "POST"])
127 | @fnauth.check_auth(
128 | 6,
129 | )
130 | def delete(id_organisme):
131 | """
132 | Route qui supprime un organisme dont l'id est donné en paramètres dans l'url
133 | Retourne une redirection vers la liste d'organismes
134 | """
135 |
136 | Bib_Organismes.delete(id_organisme)
137 | return redirect(url_for("organisme.organisms"))
138 |
139 |
140 | @route.route("organism/info/", methods=["GET"])
141 | @fnauth.check_auth(
142 | 3,
143 | )
144 | def info(id_organisme):
145 | org = Bib_Organismes.get_one(id_organisme)
146 | q = TRoles.get_all(
147 | as_model=True,
148 | params=[
149 | {"col": "active", "filter": True},
150 | {"col": "id_organisme", "filter": id_organisme},
151 | ],
152 | order_by="nom_role",
153 | )
154 | users = [data.as_dict_full_name() for data in q]
155 |
156 | return render_template("info_organisme.html", org=org, users=users)
157 |
158 |
159 | def pops(form):
160 | """
161 | Methode qui supprime les éléments indésirables du formulaires
162 | Avec pour paramètre un formulaire
163 | """
164 | form.pop("submit")
165 | form.pop("csrf_token")
166 | return form
167 |
--------------------------------------------------------------------------------
/app/templates/info_group.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 |
4 | {%set is_members = members|length > 0 %}
5 | {%set is_lists = lists|length > 0 %}
6 | {%set is_rights = rights|length > 0 %}
7 |
8 |
{{group['nom_role']}}
9 |
{{group['desc_role']}}
10 |
11 |
12 |
13 |
14 |
25 |
26 | {% if is_members %}
27 |
62 | {% else %}
63 |
Le groupe ne comporte aucun membre.
64 | {% endif %}
65 |
66 |
67 |
68 |
69 |
72 |
73 | {% if is_lists %}
74 |
86 | {% else %}
87 |
Le groupe n'appartient à aucune liste.
88 | {% endif %}
89 |
90 |
91 |
92 |
93 |
96 |
97 | {% if is_rights %}
98 |
111 | {% else %}
112 |
Le groupe ne dispose d'aucun profil dans les applications.
113 | {% endif %}
114 |
115 |
116 |
117 |
118 |
119 | Modifier le groupe
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/liste/route.py:
--------------------------------------------------------------------------------
1 | from flask import (
2 | redirect,
3 | url_for,
4 | render_template,
5 | Blueprint,
6 | request,
7 | flash,
8 | jsonify,
9 | current_app,
10 | )
11 | from pypnusershub import routes as fnauth
12 |
13 | from app.env import db
14 | from app.liste import forms as listeforms
15 | from app.models import TListes, CorRoleListe, TRoles
16 | from app.utils.utils_all import strigify_dict
17 |
18 | URL_REDIRECT = current_app.config["URL_REDIRECT"]
19 | URL_APPLICATION = current_app.config["URL_APPLICATION"]
20 |
21 |
22 | route = Blueprint("liste", __name__)
23 |
24 |
25 | @route.route("lists/list", methods=["GET", "POST"])
26 | @fnauth.check_auth(
27 | 3,
28 | )
29 | def lists():
30 | """
31 | Route qui affiche la liste des listes
32 | Retourne un template avec pour paramètres :
33 | - une entête de tableau --> fLine
34 | - le nom des colonnes de la base --> line
35 | - le contenu du tableau --> table
36 | - le chemin de mise à jour --> pathU
37 | - le chemin de suppression --> pathD
38 | - le chemin d'ajout --> pathA
39 | - le chemin des membres de la liste --> pathP
40 | - une clé (clé primaire dans la plupart des cas) --> key
41 | - un nom (nom de la table) pour le bouton ajout --> name
42 | - un nom de liste --> name_list
43 | - ajoute une colonne de bouton ('True' doit être de type string)--> otherCol
44 | - nom affiché sur le bouton --> Members
45 | """
46 |
47 | fLine = ["ID", "Code", "Nom", "Description"]
48 | columns = ["id_liste", "code_liste", "nom_liste", "desc_liste"]
49 | contents = TListes.get_all(order_by="nom_liste")
50 | return render_template(
51 | "table_database.html",
52 | fLine=fLine,
53 | line=columns,
54 | table=contents,
55 | key="id_liste",
56 | pathI=URL_APPLICATION + "/list/info/",
57 | pathU=URL_APPLICATION + "/list/update/",
58 | pathD=URL_APPLICATION + "/list/delete/",
59 | pathA=URL_APPLICATION + "/list/add/new",
60 | pathP=URL_APPLICATION + "/list/members/",
61 | name="une liste",
62 | name_list="Listes",
63 | otherCol="True",
64 | Members="Membres",
65 | see="True",
66 | )
67 |
68 |
69 | @route.route("list/add/new", defaults={"id_liste": None}, methods=["GET", "POST"])
70 | @route.route("list/update/", methods=["GET", "POST"])
71 | @fnauth.check_auth(
72 | 6,
73 | )
74 | def addorupdate(id_liste):
75 | """
76 | Route affichant un formulaire vierge ou non (selon l'url) pour ajouter ou mettre à jour une liste
77 | L'envoie du formulaire permet l'ajout ou la maj de la liste dans la base
78 | Retourne un template accompagné d'un formulaire pré-rempli ou non selon le paramètre id_liste
79 | Une fois le formulaire validé on retourne une redirection vers la liste des listes
80 | """
81 |
82 | form = listeforms.List()
83 | if id_liste == None:
84 | if request.method == "POST":
85 | if form.validate_on_submit() and form.validate():
86 | form_list = pops(form.data)
87 | form_list.pop("id_liste")
88 | TListes.post(form_list)
89 | return redirect(url_for("liste.lists"))
90 | return render_template("list.html", form=form, title="Formulaire Liste")
91 | else:
92 | list = TListes.get_one(id_liste)
93 | if request.method == "GET":
94 | form = process(form, list)
95 | if request.method == "POST":
96 | if form.validate_on_submit() and form.validate():
97 | form_list = pops(form.data)
98 | form_list["id_liste"] = list["id_liste"]
99 | TListes.update(form_list)
100 | return redirect(url_for("liste.lists"))
101 | else:
102 | flash(strigify_dict(form.errors))
103 | return render_template("list.html", form=form, title="Formulaire Liste")
104 |
105 |
106 | @route.route("list/members/", methods=["GET", "POST"])
107 | @fnauth.check_auth(
108 | 6,
109 | )
110 | def membres(id_liste):
111 | """
112 | Route affichant la liste des listes n'appartenant pas à la liste vis à vis de ceux qui appartiennent à celle-ci.
113 | Avec pour paramètre un id de liste (id_liste)
114 | Retourne un template avec pour paramètres:
115 | - une entête des tableaux --> fLine
116 | - le nom des colonnes de la base --> data
117 | - liste des listes n'appartenant pas à la liste --> table
118 | - liste des listes appartenant à la liste --> table2
119 | """
120 |
121 | users_in_list = TRoles.test_group(TRoles.get_user_in_list(id_liste))
122 | users_out_list = TRoles.test_group(TRoles.get_user_out_list(id_liste))
123 | mylist = TListes.get_one(id_liste)
124 | header = ["ID", "Nom"]
125 | data = ["id_role", "full_name"]
126 | if request.method == "POST":
127 | data = request.get_json()
128 | new_users_in_list = data["tab_add"]
129 | new_users_out_list = data["tab_del"]
130 | try:
131 | CorRoleListe.add_cor(id_liste, new_users_in_list)
132 | CorRoleListe.del_cor(id_liste, new_users_out_list)
133 | except Exception as e:
134 | return jsonify({"error": str(e)}), 500
135 | return jsonify({"redirect": url_for("liste.lists")}), 200
136 | return render_template(
137 | "tobelong.html",
138 | fLine=header,
139 | data=data,
140 | table=users_out_list,
141 | table2=users_in_list,
142 | info="Membres de la liste '" + mylist["nom_liste"] + "'",
143 | )
144 |
145 |
146 | @route.route("list/delete/", methods=["GET", "POST"])
147 | @fnauth.check_auth(
148 | 6,
149 | )
150 | def delete(id_liste):
151 | """
152 | Route qui supprime une liste dont l'id est donné en paramètres dans l'url
153 | Retourne une redirection vers la liste des listes
154 | """
155 | TListes.delete(id_liste)
156 | return redirect(url_for("liste.lists"))
157 |
158 |
159 | @route.route("list/info/", methods=["GET"])
160 | @fnauth.check_auth(3)
161 | def info(id_liste):
162 | mylist = TListes.get_one(id_liste)
163 | members = (
164 | db.session.query(CorRoleListe).filter(CorRoleListe.id_liste == id_liste).all()
165 | )
166 | return render_template("info_list.html", mylist=mylist, members=members)
167 |
168 |
169 | def pops(form):
170 | """
171 | Methode qui supprime les éléments indésirables du formulaires
172 | Avec pour paramètre un formulaire
173 | """
174 |
175 | form.pop("submit")
176 | form.pop("csrf_token")
177 | return form
178 |
179 |
180 | def process(form, list):
181 | """
182 | Methode qui rempli le formulaire par les données de l'éléments concerné
183 | Avec pour paramètres un formulaire et une liste
184 | """
185 |
186 | form.nom_liste.process_data(list["nom_liste"])
187 | form.code_liste.process_data(list["code_liste"])
188 | form.desc_liste.process_data(list["desc_liste"])
189 | return form
190 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | ============
2 | INSTALLATION
3 | ============
4 |
5 | Cette documentation décrit l'installation indépendante de UsersHub. Il est aussi possible de réaliser l'installation avec le script automatisé d'installation globale de GeoNature (http://docs.geonature.fr/installation.html#installation-globale).
6 |
7 | Prérequis
8 | =========
9 |
10 | Pour installer UsersHub, il vous faut un serveur avec :
11 |
12 | * Debian 10 ou 11
13 | * 1 Go de RAM
14 | * 5 Go d’espace disque
15 |
16 | Création d’un utilisateur
17 | =========================
18 |
19 | Vous devez disposer d'un utilisateur Linux pour faire tourner UsersHub (nommé ``synthese`` dans notre exemple). L’utilisateur doit appartenir au groupe ``sudo``. Le répertoire de cet utilisateur ``synthese`` doit être dans ``/home/synthese``. Si vous souhaitez utiliser un autre utilisateur Linux, vous devrez adapter les lignes de commande proposées dans cette documentation.
20 |
21 | ::
22 |
23 | $ adduser --home /home/synthese synthese
24 | $ adduser synthese sudo
25 |
26 | :Note:
27 |
28 | Pour la suite de l'installation, veuillez utiliser l'utilisateur Linux créé précedemment (``synthese`` dans l'exemple), et non l'utilisateur ``root``.
29 |
30 | Installation des dépendances requises
31 | =====================================
32 |
33 | Installez les dépendances suivantes :
34 |
35 | ::
36 |
37 | $ sudo apt install -y python3-venv libpq-dev postgresql apache2
38 |
39 | Installer NVM (Node version manager), Node.js et npm :
40 |
41 | ::
42 |
43 | $ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
44 |
45 | Fermer la console et la réouvrir pour que l’environnement npm soit pris en compte.
46 |
47 | Configuration de PostgresQL
48 | ===========================
49 |
50 | Créer un utilisateur PostgreSQL :
51 |
52 | ::
53 |
54 | $ sudo -u postgres psql -c "CREATE ROLE geonatuser WITH LOGIN PASSWORD 'monpassachanger';"
55 |
56 | Téléchargement de UsersHub
57 | ==========================
58 |
59 | Récupérer le zip de l'application sur le Github du projet (X.Y.Z à remplacer par la version souhaitée de UsersHub)
60 |
61 | ::
62 |
63 | $ cd /home/synthese
64 | $ wget https://github.com/PnX-SI/UsersHub/archive/X.Y.Z.zip
65 | $ unzip X.Y.Z.zip
66 | $ mv UsersHub-X.Y.Z usershub
67 |
68 | Configuration de UsersHub
69 | =========================
70 |
71 | Créer et mettre à jour le fichier ``config/settings.ini`` :
72 |
73 | ::
74 |
75 | $ cd ~/usershub
76 | $ cp config/settings.ini.sample config/settings.ini
77 | $ nano config/settings.ini
78 |
79 | Renseigner le nom de la base de données, l'utilisateur PostgreSQL et son mot de passe. Il est possible mais non conseillé de laisser les valeurs proposées par défaut.
80 |
81 | ATTENTION : Les valeurs renseignées dans ce fichier sont utilisées par le script d'installation de la base de données ``install_db.sh``. L'utilisateurs PostgreSQL doit être en concordance avec celui créé lors de la dernière étape de l'installation du serveur (``Création d'un utilisateur PostgreSQL``).
82 |
83 | :Note:
84 |
85 | Si vous installez UsersHub dans le cadre de la gestion des utilisateurs de GeoNature, il est conseillé d'utiliser les mêmes utilisateurs PostgreSQL que pour GeoNature.
86 |
87 |
88 | Configuration de l'application
89 | ==============================
90 |
91 | * Installation de l'application :
92 |
93 | ::
94 |
95 | cd ~/usershub
96 | ./install_app.sh
97 |
98 |
99 | Création et installation de la base de données
100 | ==============================================
101 |
102 | * Création de la base de données et chargement des données initiales :
103 |
104 | ::
105 |
106 | cd ~/usershub
107 | ./install_db.sh
108 |
109 |
110 | * Si vous souhaitez les données utilisateurs d’exemple, en particulier l’utilisateur ``admin`` (mot de passe : ``admin``), executez :
111 |
112 | ::
113 |
114 | cd ~/usershub
115 | source venv/bin/activate
116 | flask db upgrade usershub-samples@head
117 |
118 |
119 | Configuration Apache
120 | ====================
121 |
122 | Activez les modules ``mod_proxy`` et ``mod_proxy_http``, et redémarrez Apache :
123 |
124 | ::
125 |
126 | $ sudo a2enmod proxy proxy_http
127 | $ sudo systemctl restart apache
128 |
129 | UsersHub peut être classiquement déployé sur 2 types d’URL distincts :
130 |
131 | * Sur un préfixe : https://mon-domaine.fr/usershub/
132 | * Sur un sous-domaine : https://usershub.mon-domaine.fr
133 |
134 | Installation de UsersHub sur un préfixe
135 | ---------------------------------------
136 |
137 | Le processus d’installation de l’application créer le fichier de configuration Apache ``/etc/apache2/conf-available/usershub.conf`` permettant de servir UsersHub sur le préfixe ``/usershub/``. Pour activer ce fichier de configuration, exécutez les commandes suivantes :
138 |
139 | ::
140 |
141 | sudo a2enconf usershub
142 | sudo service apache2 reload
143 |
144 | Installation de UsersHub sur un sous-domaine
145 | --------------------------------------------
146 |
147 | Dans le cas où UsersHub est installé sur un sous-domaine et non sur un préfixe (c’est-à-dire ``https://usershub.mon-domaine.fr``), veuillez ajouter dans le fichier de configuration de votre virtualhost (*e.g.* ``/etc/apache2/sites-enabled/usershub.conf``) la section suivante :
148 |
149 | ::
150 |
151 |
152 | ProxyPass http://127.0.0.1:5001/
153 | ProxyPassReverse http://127.0.0.1:5001/
154 |
155 |
156 |
157 | Configuration de l'application
158 | ==============================
159 |
160 | La configuration de UsersHub est réalisée dans le fichier ``config/config.py``.
161 |
162 | Si vous modifiez les paramètres dans ce fichier, vous devez recharger l'application avec la commande ``systemctl reload usershub``.
163 |
164 | Mise à jour de l'application
165 | ============================
166 |
167 | * Télécharger la dernière version de UsersHub
168 |
169 | ::
170 |
171 | cd
172 | wget https://github.com/PnX-SI/UsersHub/archive/X.Y.Z.zip
173 | unzip X.Y.Z.zip
174 | rm X.Y.Z.zip
175 |
176 | * Renommer l’ancien répertoire de l’application, ainsi que le nouveau :
177 |
178 | ::
179 |
180 | mv /home/`whoami`/usershub/ /home/`whoami`/usershub_old/
181 | mv UsersHub-X.Y.Z /home/`whoami`/usershub/
182 |
183 | * Récupérer les fichiers de configuration de la version précedente :
184 |
185 | ::
186 |
187 | cp /home/`whoami`/usershub_old/config/config.py /home/`whoami`/usershub/config/config.py
188 | cp /home/`whoami`/usershub_old/config/settings.ini /home/`whoami`/usershub/config/settings.ini
189 |
190 | * Lancer le script d'installation de l'application (attention si vous avez modifiez certains paramètres dans le fichier ``config.py`` tels que les paramètres de connexion à la base de données, ils seront écrasés par les paramètres présent dans le fichier ``settings.ini``) :
191 |
192 | ::
193 |
194 | cd usershub
195 | ./install_app.sh
196 |
197 | * Si vous utilisez UsersHub tout seul (sans GeoNature), mettre à jour sa base de données :
198 |
199 | ::
200 |
201 | cd usershub
202 | source venv/bin/activate
203 | flask db upgrade usershub@head
204 | flask db upgrade utilisateurs@head
205 |
206 | * Suivre les éventuelles notes de version spécifiques à chaque version
207 |
--------------------------------------------------------------------------------
/app/templates/table_database.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 | {% include "alert_messages.html" %}
4 |
5 | {% block content %}
6 |
7 |
8 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% for name in fLine %}
28 | {{ name }}
29 | {% endfor %}
30 | {% if passPlusCol == 'True' or passMd5Col %}
31 |
32 |
33 |
34 | {% endif %}
35 | {% if otherCol == 'True' %}
36 |
37 |
38 |
39 | {% endif %}
40 | {% if tag_orga == 'True' %}
41 | {{ Organismes }}
42 | {% endif %}
43 | {% if permissions == 'True' %}
44 |
45 | {% endif %}
46 | {% if see == 'True' %}
47 |
48 | {% endif %}
49 |
50 |
51 |
52 |
53 |
54 | {% for elt in table %}
55 |
56 | {% for name in line %}
57 | {% set groups = elt[group] %}
58 | {% if groups == 'True' %}
59 | {{ elt[name] }}
60 | {% else %}
61 | {{ elt[name] }}
62 | {% endif %}
63 | {% endfor %}
64 |
65 | {% if passPlusCol == 'True' or passMd5Col == 'True' %}
66 |
67 | {% if elt['pass_plus'] == 'Oui' or elt['pass_md5'] == 'Oui'%}
68 | {% set pass_plus_action = 'Modifier le mot de passe' %}
69 | {% set pass_class = 'secondary' %}
70 | {% else %}
71 | {% set pass_plus_action = 'Ajouter un mot de passe' %}
72 | {% set pass_class = 'light' %}
73 | {% endif %}
74 |
75 |
76 |
77 |
78 |
79 |
80 | {% endif %}
81 |
82 | {% if otherCol == 'True' %}
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {% endif %}
91 |
92 | {% if permissions == 'True' %}
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | {% endif %}
101 |
102 | {% if see == 'True' %}
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | {% endif %}
111 |
112 |
113 | {% if key2 %}
114 |
115 | {% else %}
116 |
117 | {% endif %}
118 |
119 |
120 |
121 |
122 |
123 |
124 | {% if key2 %}
125 |
126 | {% else %}
127 |
128 | {% endif %}
129 |
130 |
131 |
132 |
133 | {% endfor %}
134 |
135 |
136 |
137 |
138 |
139 | {% endblock content %}
--------------------------------------------------------------------------------
/app/groupe/route.py:
--------------------------------------------------------------------------------
1 | from flask import (
2 | redirect,
3 | url_for,
4 | render_template,
5 | Blueprint,
6 | request,
7 | flash,
8 | jsonify,
9 | current_app,
10 | )
11 | from pypnusershub import routes as fnauth
12 |
13 | from app.groupe import forms as groupeforms
14 | from app.models import TRoles
15 | from app.models import CorRoles
16 | from app.utils.utils_all import strigify_dict
17 |
18 |
19 | URL_REDIRECT = current_app.config["URL_REDIRECT"]
20 | URL_APPLICATION = current_app.config["URL_APPLICATION"]
21 |
22 | route = Blueprint("groupe", __name__)
23 |
24 |
25 | @route.route("groups/list", methods=["GET", "POST"])
26 | @fnauth.check_auth(
27 | 3,
28 | )
29 | def groups():
30 | """
31 | Route qui affiche la liste des groupes
32 | Retourne un template avec pour paramètres :
33 | - une entête de tableau --> fLine
34 | - le nom des colonnes de la base --> line
35 | - le contenu du tableau --> table
36 | - le chemin de mise à jour --> pathU
37 | - le chemin de suppression --> pathD
38 | - le chemin d'ajout --> pathA
39 | - le chemin des membres du groupe --> pathP
40 | - une clé (clé primaire dans la plupart des cas) --> key
41 | - un nom (nom de la table) pour le bouton ajout --> name
42 | - un nom de listes --> name_list
43 | - ajoute une colonne de bouton ('True' doit être de type string)--> otherCol
44 | - nom affiché sur le bouton --> Members
45 | """
46 |
47 | fLine = ["ID groupe", "nom", "description"]
48 | columns = ["id_role", "nom_role", "desc_role"]
49 | filters = [{"col": "groupe", "filter": "True"}]
50 | contents = TRoles.get_all(columns, filters, order_by="identifiant")
51 | return render_template(
52 | "table_database.html",
53 | fLine=fLine,
54 | line=columns,
55 | table=contents,
56 | key="id_role",
57 | pathI=URL_APPLICATION + "/group/info/",
58 | pathU=URL_APPLICATION + "/group/update/",
59 | pathD=URL_APPLICATION + "/group/delete/",
60 | pathA=URL_APPLICATION + "/group/add/new",
61 | pathP=URL_APPLICATION + "/group/members/",
62 | name="un groupe",
63 | name_list="Groupes",
64 | otherCol="True",
65 | Members="Membres",
66 | see="True",
67 | )
68 |
69 |
70 | @route.route("group/add/new", methods=["GET", "POST"])
71 | @route.route("group/update/", methods=["GET", "POST"])
72 | @fnauth.check_auth(
73 | 6,
74 | )
75 | def addorupdate(id_role=None):
76 | """
77 | Route affichant un formulaire vierge ou non (selon l'url) pour ajouter ou mettre à jour un groupe
78 | L'envoie du formulaire permet l'ajout ou la maj du groupe dans la base
79 | Retourne un template accompagné d'un formulaire pré-rempli ou non selon le paramètre id_role
80 | Une fois le formulaire validé on retourne une redirection vers la liste de groupe
81 | """
82 | form = groupeforms.Group()
83 | form.groupe.process_data(True)
84 | if id_role == None:
85 | if request.method == "POST":
86 | if form.validate_on_submit() and form.validate():
87 | form_group = pops(form.data)
88 | form_group.pop("id_role")
89 | # set the group as active default
90 | form_group["active"] = True
91 | TRoles.post(form_group)
92 | return redirect(url_for("groupe.groups"))
93 | else:
94 | errors = form.errors
95 | return render_template("group.html", form=form, title="Formulaire Groupe")
96 | else:
97 | group = TRoles.get_one(id_role)
98 | if request.method == "GET":
99 | form = process(form, group)
100 | if request.method == "POST":
101 | if form.validate_on_submit() and form.validate():
102 | form_group = pops(form.data)
103 | form_group["id_role"] = group["id_role"]
104 | TRoles.update(form_group)
105 | return redirect(url_for("groupe.groups"))
106 | else:
107 | errors = form.errors
108 | flash(strigify_dict(errors), "error")
109 | return render_template("group.html", form=form, title="Formulaire Groupe")
110 |
111 |
112 | @route.route("group/members/", methods=["GET", "POST"])
113 | @fnauth.check_auth(
114 | 6,
115 | )
116 | def membres(id_groupe):
117 | """
118 | Route affichant la liste des roles n'appartenant pas au groupe vis à vis de ceux qui appartiennent à celui ci.
119 | Avec pour paramètre un id de groupe (id_role)
120 | Retourne un template avec pour paramètres:
121 | - une entête des tableaux --> fLine
122 | - le nom des colonnes de la base --> data
123 | - liste des roles n'appartenant pas au groupe --> table
124 | - liste des roles appartenant au groupe --> table2
125 | - variable qui permet a jinja de colorer une ligne si celui-ci est un groupe --> group
126 | """
127 |
128 | users_in_group = TRoles.test_group(TRoles.get_user_in_group(id_groupe))
129 | users_out_group = TRoles.test_group(TRoles.get_user_out_group(id_groupe))
130 | group = TRoles.get_one(id_groupe)
131 | header = ["ID", "Nom"]
132 | data = ["id_role", "full_name"]
133 | if request.method == "POST":
134 | data = request.get_json()
135 | new_users_in_group = data["tab_add"]
136 | new_users_out_group = data["tab_del"]
137 | try:
138 | CorRoles.add_cor(id_groupe, new_users_in_group)
139 | CorRoles.del_cor(id_groupe, new_users_out_group)
140 | except Exception as e:
141 | return jsonify(str(e)), 500
142 | return jsonify({"redirect": url_for("groupe.groups")}), 200
143 | return render_template(
144 | "tobelong.html",
145 | fLine=header,
146 | data=data,
147 | table=users_out_group,
148 | table2=users_in_group,
149 | group="groupe",
150 | info="Membres du groupe '" + group["nom_role"] + "'",
151 | )
152 |
153 |
154 | @route.route("group/delete/", methods=["GET", "POST"])
155 | @fnauth.check_auth(
156 | 6,
157 | )
158 | def delete(id_groupe):
159 | """
160 | Route qui supprime un groupe dont l'id est donné en paramètres dans l'url
161 | Retourne une redirection vers la liste de groupe
162 | """
163 |
164 | TRoles.delete(id_groupe)
165 | return redirect(url_for("groupe.groups"))
166 |
167 |
168 | @route.route("group/info/", methods=["GET", "POST"])
169 | @fnauth.check_auth(
170 | 3,
171 | )
172 | def info(id_role):
173 | group = TRoles.get_one(id_role)
174 | members = TRoles.get_user_in_group(id_role)
175 | lists = TRoles.get_user_lists(id_role)
176 | rights = TRoles.get_user_app_profils(id_role)
177 | return render_template(
178 | "info_group.html",
179 | group=group,
180 | members=members,
181 | lists=lists,
182 | rights=rights,
183 | pathU=URL_APPLICATION + "/group/update/",
184 | )
185 |
186 |
187 | def pops(form):
188 | """
189 | Methode qui supprime les éléments indésirables du formulaires
190 | Avec pour paramètre un formulaire
191 | """
192 |
193 | form.pop("submit")
194 | form.pop("csrf_token")
195 | return form
196 |
197 |
198 | def process(form, group):
199 | """
200 | Methode qui rempli le formulaire par les données de l'éléments concerné
201 | Avec pour paramètres un formulaire et un groupe
202 | """
203 |
204 | form.nom_role.process_data(group["nom_role"])
205 | form.desc_role.process_data(group["desc_role"])
206 | return form
207 |
--------------------------------------------------------------------------------
/app/static/transfer.js:
--------------------------------------------------------------------------------
1 | var tab_add = [];
2 | var tab_del = [];
3 | var data_select = [];
4 | var profil = [];
5 |
6 | function fill_select() {
7 | td_profil =
8 | '';
9 | for (var i = 0; i < data_select.length; i++) {
10 | td_profil =
11 | td_profil +
12 | '' +
15 | data_select[i]["nom_profil"] +
16 | " ";
17 | }
18 | td_profil = td_profil + " ";
19 | return td_profil;
20 | }
21 |
22 | function addTab(tab, table) {
23 | for (var i = 0; i < tab.length; i++) {
24 | table.append(tab[i]);
25 | }
26 | }
27 |
28 | var add = function(app) {
29 | var tab = [];
30 | i = 0;
31 | $('#user input[type="checkbox"]:checked').each(function() {
32 | if (app != null) {
33 | var Row = $(this)
34 | .parents("tr")
35 | .append(fill_select());
36 | } else {
37 | var Row = $(this)
38 | .parents("tr")
39 | .append();
40 | }
41 | tab.push(Row[0]);
42 | $("#user")
43 | .find("input[type=checkbox]:checked")
44 | .prop("checked", false);
45 | var ID = $(this)
46 | .parents("tr")
47 | .find("td:eq(1)")
48 | .html();
49 | tab_add.push(ID);
50 |
51 | if (isInTabb(tab_del, ID) == true) {
52 | tab_del.splice(tab_del.indexOf(ID), 1);
53 | tab_add.splice(tab_add.indexOf(ID), 1);
54 | }
55 | });
56 | var table = $("#adding_table");
57 | addTab(tab, table);
58 | };
59 |
60 | var del = function(app) {
61 | var tab = [];
62 | $('#adding_table input[type="checkbox"]:checked').each(function() {
63 | var Row = $(this).parents("tr");
64 | var ID = $(this)
65 | .parents("tr")
66 | .find("td:eq(1)")
67 | .html();
68 | var PROFIL = $(this)
69 | .parents("tr")
70 | .find("option:selected")
71 | .val();
72 | if (app != null) {
73 | profil.push({ id_role: ID, id_profil: PROFIL });
74 | Row.find(".profil").remove();
75 | }
76 | var Row = $(this).parents("tr");
77 | tab.push(Row[0]);
78 | $("#adding_table")
79 | .find("input[type=checkbox]:checked")
80 | .prop("checked", false);
81 | tab_del.push(ID);
82 | if (isInTabb(tab_add, ID) == true) {
83 | tab_add.splice(tab_add.indexOf(ID), 1);
84 | tab_del.splice(tab_del.indexOf(ID), 1);
85 | }
86 | });
87 | var table = $("#user");
88 | addTab(tab, table);
89 | };
90 |
91 | var get_profil = function(data) {
92 | var tab = [];
93 | data_id = data["tab_add"];
94 | $("#adding_table tr").each(function() {
95 | var ID = $(this)
96 | .find("td:eq(1)")
97 | .html();
98 | var PROFIL = $(this)
99 | .find("option:selected")
100 | .val();
101 | for (d in data_id) {
102 | if (ID == data_id[d]) {
103 | tab.push({ id_role: ID, id_profil: PROFIL });
104 | }
105 | //tab.push({ id_role: ID, id_profil: PROFIL });
106 | }
107 | });
108 | return tab;
109 | };
110 |
111 | var get_profil_delete = function(data) {
112 | for (r in profil) {
113 | if (isInTabb(data["tab_del"], r["id_role"]) == true) {
114 | profil.splice(profil.indexOf(r), 1);
115 | }
116 | }
117 | };
118 |
119 | // TODO: pourquoi faire une requeste AJAX pour ce post ?
120 | // est-ce qu'on pourrait pas utiliser un formulaire simple ?
121 | var update_right = function() {
122 | var data = {};
123 | data["tab_add"] = tab_add;
124 | data["tab_del"] = tab_del;
125 | data["tab_add"] = get_profil(data);
126 | get_profil_delete(data);
127 | data["tab_del"] = profil;
128 | $.ajax({
129 | url: $(location).attr("href"),
130 | type: "post",
131 | data: JSON.stringify(data),
132 | contentType: "application/json; charset=utf-8",
133 | dataType: "json"
134 | })
135 | .done(function(data) {
136 | //window.location.href = data.redirect;
137 | })
138 | .fail(function(data) {
139 | alert("Une erreur c'est produite");
140 | });
141 |
142 | tab_add = [];
143 | tab_del = [];
144 | tab_profil = [];
145 | };
146 |
147 | var update = function() {
148 | var data = {};
149 | data["tab_add"] = tab_add;
150 | data["tab_del"] = tab_del;
151 |
152 | $.ajax({
153 | url: $(location).attr("href"),
154 | type: "post",
155 | data: JSON.stringify(data),
156 | contentType: "application/json; charset=utf-8",
157 | dataType: "json"
158 | })
159 | .done(function(data) {
160 | window.location.href = data.redirect;
161 | })
162 | .fail(function(data) {
163 | console.log(data);
164 | alert("Une erreur s'est produite");
165 | });
166 |
167 | tab_add = [];
168 | tab_del = [];
169 | };
170 |
171 | function isInTabb(tab, id) {
172 | var bool = false;
173 | tab.forEach(element => {
174 | if (element == id) {
175 | bool = true;
176 | }
177 | });
178 | return bool;
179 | }
180 |
181 | var deleteRaw = function(path) {
182 | var c = confirm("Etes vous sur de vouloir supprimer cet élement ? ");
183 | if (c == true) window.location.href = path;
184 | };
185 |
186 | // MAIN executed
187 |
188 | // get the app profils
189 | // TODO: ne devrait être déclenché que quand on en a besoin
190 | // parse the url to get the id_application
191 | var current_url = window.location.href;
192 | url_array = current_url.split("/");
193 | if (
194 | url_array.indexOf("application") != -1 &&
195 | url_array.indexOf("rights") != -1
196 | ) {
197 | id_application = url_array[url_array.length - 1];
198 | console.log(id_application);
199 | $.ajax({
200 | url: url_app + "/api/profils?id_application=" + id_application,
201 | type: "get",
202 | data: JSON.stringify(data_select),
203 | contentType: "application/json; charset=utf-8",
204 | dataType: "json",
205 | success: function(response) {
206 | data_select = response;
207 | },
208 | error: function(error) {}
209 | });
210 | }
211 |
212 | $(document).ready(function() {
213 | var tab_add = [];
214 | var tab_del = [];
215 | var data_select = {};
216 |
217 | // init the left table
218 | $("#user").DataTable({
219 | language: {
220 | lengthMenu: "Afficher _MENU_ éléments par page",
221 | zeroRecords: "Aucune donnée",
222 | info: "Affiche la page _PAGE_ sur _PAGES_",
223 | infoEmpty: "Aucune donnée",
224 | infoFiltered: "(filtrer sur _MAX_ total d'éléments)",
225 | search: "Recherche:",
226 | paginate: {
227 | first: "Première",
228 | last: "Dernière",
229 | next: "Suivante",
230 | previous: "Précédente"
231 | },
232 | aLengthMenu: [[10, 25, 50, 75, -1], [10, 25, 50, 75, "All"]],
233 | iDisplayLength: 25
234 | }
235 | });
236 |
237 | // init the right table
238 | $("#adding_table").DataTable({
239 | language: {
240 | lengthMenu: "Afficher _MENU_ éléments par page",
241 | zeroRecords: "Aucune donnée",
242 | info: "Affiche la page _PAGE_ sur _PAGES_",
243 | infoEmpty: "Aucune donnée",
244 | infoFiltered: "(filtrer sur _MAX_ total d'éléments)",
245 | search: "Recherche:",
246 | paginate: {
247 | first: "Première",
248 | last: "Dernière",
249 | next: "Suivante",
250 | previous: "Précédente"
251 | },
252 | aLengthMenu: [[10, 25, 50, 75, -1], [10, 25, 50, 75, "All"]],
253 | iDisplayLength: 25
254 | }
255 | });
256 |
257 | // init table from table_database.html
258 | $("#tri").DataTable({
259 | language: {
260 | lengthMenu: "Afficher _MENU_ éléments par page",
261 | zeroRecords: "Aucune donnée",
262 | info: "Affiche la page _PAGE_ sur _PAGES_",
263 | infoEmpty: "Aucune donnée",
264 | infoFiltered: "(filtrer sur _MAX_ total d'éléments)",
265 | search: "Recherche:",
266 | paginate: {
267 | first: "Première",
268 | last: "Dernière",
269 | next: "Suivante",
270 | previous: "Précédente"
271 | },
272 | aLengthMenu: [[10, 25, 50, 75, -1], [10, 25, 50, 75, "All"]],
273 | iDisplayLength: 25
274 | },
275 | aaSorting: []
276 | });
277 |
278 | // end main
279 | });
280 |
--------------------------------------------------------------------------------
/app/templates/info_user.html:
--------------------------------------------------------------------------------
1 | {% include "librairies.html" %}
2 | {% include "head-appli.html" %}
3 |
4 | {%set is_identifiant = user['identifiant'] is not none and user['identifiant'] != '' %}
5 | {%set is_uuid = user['uuid_role'] is not none and user['uuid_role'] != '' %}
6 | {%set is_organisme = organisme is not none and organisme['nom_organisme'] != '' %}
7 | {%set is_desc = user['desc_role'] is not none and user['desc_role'] != '' %}
8 | {%set is_remarques = user['remarques'] is not none and user['remarques'] != '' %}
9 | {%set is_orga_champ_addi = user['champs_addi']['organisme'] is not none
10 | and user['champs_addi']['organisme'] != '' %}
11 | {%set is_date_insert = user['date_insert'] is not none and user['date_insert'] != '' %}
12 | {%set is_date_update = user['date_update'] is not none and user['date_update'] != '' %}
13 | {%set is_champs_addi = user['champs_addi'] is not none and user['champs_addi'] != '' %}
14 | {%set is_mail = user['email'] is not none and user['email'] != '' %}
15 | {%set is_group = groups|length > 0 %}
16 | {%set is_list = lists|length > 0 %}
17 | {%set is_right = rights|length > 0 %}
18 |
19 |
Utilisateur "{{fullname}}"
20 |
21 |
22 | {% if is_desc %}
23 | {{user['desc_role']}}
24 | {% endif %}
25 | {% if is_uuid %}
26 | UUID : {{user['uuid_role']}}
27 | {% endif %}
28 | {% if is_identifiant %}
29 | Identifiant : {{user['identifiant']}}
30 | {% endif %}
31 | {% if is_mail %}
32 | Email : {{user['email']}}
33 | {% endif %}
34 | {% if is_organisme %}
35 | Organisme : {{organisme.nom_organisme}}
36 | {% elif is_orga_champ_addi %}
37 | Organisme : {{user['champs_addi']['organisme']}}
38 | {% endif %}
39 | {% if is_remarques %}
40 | Remarques : {{user['remarques']}}
41 | {% endif %}
42 |
43 |
44 | Mot de passe Plus : {{"Non" if user['pass_plus'] == "" or user["pass_plus"] is none else "Oui"}}
45 | Mot de passe Md5 : {{"Non" if user['pass_md5'] == "" or user["pass_md5"] is none else "Oui"}}
46 | Compte Actif : {{"Oui" if user["active"] else "Non" }}
47 | {% if is_date_insert %}
48 | Date de création : {{user['date_insert']|truncate(10, True, '', 0)}}
49 | {% endif %}
50 | {% if is_date_update %}
51 | Date de mise à jour : {{user['date_update']|truncate(10, True, '', 0)}}
52 | {% endif %}
53 | {% if is_champs_addi %}
54 |
55 | {% for key, value in user.champs_addi.items() %}
56 | {{ key|pretty_json_key|capitalize }} :
57 |
58 | {% if value is iterable and (value is not string and value is not mapping) %}
59 |
60 | {% for item in value %}
61 | {{ item }}
62 | {% endfor %}
63 |
64 | {% elif value is mapping %}
65 |
66 | {% for key, value in value.items() %}
67 | {{ key|e|capitalize }} :
68 | {{ value }}
69 | {% endfor %}
70 |
71 | {% else %}
72 | {{ value }}
73 | {% endif %}
74 |
75 | {% endfor %}
76 |
77 | {% endif %}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
88 |
89 | {%if is_group %}
90 |
102 | {% else %}
103 |
L'utilisateur n'appartient à aucun groupe.
104 | {% endif %}
105 |
106 |
107 |
108 |
109 |
112 |
113 | {%if is_list %}
114 |
126 | {% else %}
127 |
L'utilisateur n'appartient à aucune liste.
128 | {% endif %}
129 |
130 |
131 |
132 |
133 |
136 |
137 | {%if is_right %}
138 |
151 | {% else %}
152 |
L'utilisateur ne dispose d'aucune autorisation dans les applications.
153 | {% endif %}
154 |
155 |
156 |
157 |
158 |
159 | Modifier l'utilisateur
160 |
161 |
162 |
--------------------------------------------------------------------------------
/app/utils/utilssqlalchemy.py:
--------------------------------------------------------------------------------
1 | """
2 | Fonctions utilitaires
3 | """
4 |
5 | import json
6 | from functools import wraps
7 |
8 | from dateutil import parser
9 | from flask import Response, current_app
10 | from werkzeug.datastructures import Headers
11 |
12 | from sqlalchemy.dialects.postgresql import UUID
13 | from sqlalchemy import create_engine, MetaData
14 | from sqlalchemy.orm import ColumnProperty
15 |
16 |
17 | # def testDataType(value, sqlType, paramName):
18 | # if sqlType == DB.Integer or isinstance(sqlType, (DB.Integer)):
19 | # try:
20 | # int(value)
21 | # except Exception as e:
22 | # return '{0} must be an integer'.format(paramName)
23 | # if sqlType == DB.Numeric or isinstance(sqlType, (DB.Numeric)):
24 | # try:
25 | # float(value)
26 | # except Exception as e:
27 | # return '{0} must be an float (decimal separator .)'\
28 | # .format(paramName)
29 | # elif sqlType == DB.DateTime or isinstance(sqlType, (DB.Date, DB.DateTime)):
30 | # try:
31 | # dt = parser.parse(value)
32 | # except Exception as e:
33 | # return '{0} must be an date (yyyy-mm-dd)'.format(paramName)
34 | # return None
35 |
36 | """
37 | Liste des types de donnees sql qui
38 | necessite une sérialisation particulière en
39 | MANQUE FLOAT
40 | """
41 | SERIALIZERS = {
42 | "date": lambda x: str(x) if x else None,
43 | "datetime": lambda x: str(x) if x else None,
44 | "time": lambda x: str(x) if x else None,
45 | "timestamp": lambda x: str(x) if x else None,
46 | "uuid": lambda x: str(x) if x else None,
47 | }
48 |
49 |
50 | class GenericTable:
51 | """
52 | Classe permettant de créer à la volée un mapping
53 | d'une vue avec la base de données par rétroingénierie
54 | """
55 |
56 | def __init__(self, tableName, schemaName, geometry_field):
57 | meta = MetaData(schema=schemaName, bind=DB.engine)
58 | meta.reflect(views=True)
59 | try:
60 | self.tableDef = meta.tables["{}.{}".format(schemaName, tableName)]
61 | except KeyError:
62 | raise KeyError("table doesn't exists")
63 |
64 | self.geometry_field = geometry_field
65 |
66 | # Mise en place d'un mapping des colonnes en vue d'une sérialisation
67 | self.serialize_columns = [
68 | (name, SERIALIZERS.get(db_col.type.__class__.__name__.lower(), lambda x: x))
69 | for name, db_col in self.tableDef.columns.items()
70 | if not db_col.type.__class__.__name__ == "Geometry"
71 | ]
72 | self.columns = [column.name for column in self.tableDef.columns]
73 |
74 | def as_dict(self, data):
75 | return {
76 | item: _serializer(getattr(data, item))
77 | for item, _serializer in self.serialize_columns
78 | }
79 |
80 |
81 | def serializeQuery(data, columnDef):
82 | rows = [
83 | {
84 | c["name"]: getattr(row, c["name"])
85 | for c in columnDef
86 | if getattr(row, c["name"]) is not None
87 | }
88 | for row in data
89 | ]
90 | return rows
91 |
92 |
93 | def serializeQueryTest(data, columnDef):
94 | rows = list()
95 | for row in data:
96 | inter = {}
97 | for c in columnDef:
98 | if getattr(row, c["name"]) is not None:
99 | if isinstance(c["type"], (DB.Date, DB.DateTime, UUID)):
100 | inter[c["name"]] = str(getattr(row, c["name"]))
101 | elif isinstance(c["type"], DB.Numeric):
102 | inter[c["name"]] = float(getattr(row, c["name"]))
103 | elif not isinstance(c["type"], Geometry):
104 | inter[c["name"]] = getattr(row, c["name"])
105 | rows.append(inter)
106 | return rows
107 |
108 |
109 | def serializeQueryOneResult(row, columnDef):
110 | row = {
111 | c["name"]: getattr(row, c["name"])
112 | for c in columnDef
113 | if getattr(row, c["name"]) is not None
114 | }
115 | return row
116 |
117 |
118 | def serializable(cls):
119 | """
120 | Décorateur de classe pour les DB.Models
121 | Permet de rajouter la fonction as_dict qui est basée sur le mapping SQLAlchemy
122 | """
123 |
124 | """
125 | Liste des propriétés sérialisables de la classe
126 | associées à leur sérializer en fonction de leur type
127 | """
128 | cls_db_columns = []
129 | for prop in cls.__mapper__.column_attrs:
130 | if isinstance(prop, ColumnProperty) and len(prop.columns) == 1:
131 | db_col = prop.columns[0]
132 | # HACK
133 | # -> Récupération du nom de l'attribut sans la classe
134 | name = str(prop).split(".", 1)[1]
135 | if not db_col.type.__class__.__name__ == "Geometry":
136 | cls_db_columns.append(
137 | (
138 | name,
139 | SERIALIZERS.get(
140 | db_col.type.__class__.__name__.lower(), lambda x: x
141 | ),
142 | )
143 | )
144 |
145 | """
146 | Liste des propriétés synonymes
147 | sérialisables de la classe
148 | associées à leur sérializer en fonction de leur type
149 | """
150 | for syn in cls.__mapper__.synonyms:
151 | col = cls.__mapper__.c[syn.name]
152 | # if column type is geometry pass
153 | if col.type.__class__.__name__ == "Geometry":
154 | pass
155 |
156 | # else add synonyms in columns properties
157 | cls_db_columns.append(
158 | (syn.key, SERIALIZERS.get(col.type.__class__.__name__.lower(), lambda x: x))
159 | )
160 |
161 | """
162 | Liste des propriétés de type relationship
163 | uselist permet de savoir si c'est une collection de sous objet
164 | sa valeur est déduite du type de relation
165 | (OneToMany, ManyToOne ou ManyToMany)
166 | """
167 | cls_db_relationships = [
168 | (db_rel.key, db_rel.uselist) for db_rel in cls.__mapper__.relationships
169 | ]
170 |
171 | def serializefn(self, recursif=False, columns=()):
172 | """
173 | Méthode qui renvoie les données de l'objet sous la forme d'un dict
174 | Parameters
175 | ----------
176 | recursif: boolean
177 | Spécifie si on veut que les sous objet (relationship)
178 | soit également sérialisé
179 | columns: liste
180 | liste des colonnes qui doivent être prises en compte
181 | """
182 | if columns:
183 | fprops = list(filter(lambda d: d[0] in columns, cls_db_columns))
184 | else:
185 | fprops = cls_db_columns
186 |
187 | out = {item: _serializer(getattr(self, item)) for item, _serializer in fprops}
188 |
189 | if recursif is False:
190 | return out
191 | for rel, uselist in cls_db_relationships:
192 | if getattr(self, rel) is None:
193 | break
194 |
195 | if uselist is True:
196 | out[rel] = [x.as_dict(recursif) for x in getattr(self, rel)]
197 | else:
198 | out[rel] = getattr(self, rel).as_dict(recursif)
199 |
200 | return out
201 |
202 | cls.as_dict = serializefn
203 | return cls
204 |
205 |
206 | def json_resp(fn):
207 | """
208 | Décorateur transformant le résultat renvoyé par une vue
209 | en objet JSON
210 | """
211 |
212 | @wraps(fn)
213 | def _json_resp(*args, **kwargs):
214 | res = fn(*args, **kwargs)
215 | if isinstance(res, tuple):
216 | res, status = res
217 | else:
218 | status = 200
219 |
220 | if not res:
221 | status = 404
222 | res = {"message": "not found"}
223 |
224 | return Response(json.dumps(res), status=status, mimetype="application/json")
225 |
226 | return _json_resp
227 |
228 |
229 | def csv_resp(fn):
230 | """
231 | Décorateur transformant le résultat renvoyé en un fichier csv
232 | """
233 |
234 | @wraps(fn)
235 | def _csv_resp(*args, **kwargs):
236 | res = fn(*args, **kwargs)
237 | filename, data, columns, separator = res
238 | outdata = [separator.join(columns)]
239 |
240 | headers = Headers()
241 | headers.add("Content-Type", "text/plain")
242 | headers.add(
243 | "Content-Disposition", "attachment", filename="export_%s.csv" % filename
244 | )
245 |
246 | for o in data:
247 | outdata.append(
248 | separator.join(
249 | '"%s"' % (o.get(i), "")[o.get(i) is None] for i in columns
250 | )
251 | )
252 | out = "\r\n".join(outdata)
253 | return Response(out, headers=headers)
254 |
255 | return _csv_resp
256 |
--------------------------------------------------------------------------------
/app/templates/wtf_bootstrap_4.html:
--------------------------------------------------------------------------------
1 | {% macro form_errors(form, hiddens=True) %}
2 | {%- if form.errors %}
3 | {%- for fieldname, errors in form.errors.items() %}
4 | {%- if bootstrap_is_hidden_field(form[fieldname]) and hiddens or
5 | not bootstrap_is_hidden_field(form[fieldname]) and hiddens != 'only' %}
6 | {%- for error in errors %}
7 | {{error}}
8 | {%- endfor %}
9 | {%- endif %}
10 | {%- endfor %}
11 | {%- endif %}
12 | {%- endmacro %}
13 |
14 | {% macro _hz_form_wrap(horizontal_columns, form_type, add_group=False, required=False) %}
15 | {% if form_type == "horizontal" %}
16 | {% if add_group %}
26 | {% endif %}
27 | {% endmacro %}
28 |
29 | {% macro form_field(field,
30 | form_type="basic",
31 | horizontal_columns=('lg', 2, 10),
32 | button_map={}) %}
33 |
34 | {# this is a workaround hack for the more straightforward-code of just passing required=required parameter. older versions of wtforms do not have
35 | the necessary fix for required=False attributes, but will also not set the required flag in the first place. we skirt the issue using the code below #}
36 | {% if field.flags.required and not required in kwargs %}
37 | {% set kwargs = dict(required=True, **kwargs) %}
38 | {% endif %}
39 |
40 | {% if field.widget.input_type == 'checkbox' %}
41 | {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %}
42 |
43 |
44 | {{field()|safe}} {{field.label.text|safe}}
45 |
46 |
47 | {% endcall %}
48 | {%- elif field.type == 'RadioField' -%}
49 | {# note: A cleaner solution would be rendering depending on the widget,
50 | this is just a hack for now, until I can think of something better #}
51 | {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %}
52 | {% for item in field -%}
53 |
54 |
55 | {{item|safe}} {{item.label.text|safe}}
56 |
57 |
58 | {% endfor %}
59 | {% endcall %}
60 | {%- elif field.type == 'SubmitField' -%}
61 | {# deal with jinja scoping issues? #}
62 | {% set field_kwargs = kwargs %}
63 |
64 | {# note: same issue as above - should check widget, not field type #}
65 | {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %}
66 | {{field(class='btn btn-%s' % button_map.get(field.name, 'default'),
67 | **field_kwargs)}}
68 | {% endcall %}
69 | {%- elif field.type == 'FormField' -%}
70 | {# note: FormFields are tricky to get right and complex setups requiring
71 | these are probably beyond the scope of what this macro tries to do.
72 | the code below ensures that things don't break horribly if we run into
73 | one, but does not try too hard to get things pretty. #}
74 |
75 | {{field.label}}
76 | {%- for subfield in field %}
77 | {% if not bootstrap_is_hidden_field(subfield) -%}
78 | {{ form_field(subfield,
79 | form_type=form_type,
80 | horizontal_columns=horizontal_columns,
81 | button_map=button_map) }}
82 | {%- endif %}
83 | {%- endfor %}
84 |
85 | {% else -%}
86 |
87 | {%- if form_type == "inline" %}
88 |
89 | {{field.label(class="sr-only")|safe}}
90 |
91 |
92 | {% if field.type == 'FileField' %}
93 |
94 | {{field(**kwargs)|safe}}
95 | {% else %}
96 | {% if field.errors %}
97 |
error
98 | {{ field(class="form-control is-invalid", **kwargs)|safe }}
99 | {% else %}
100 | {{ field(class="form-control", **kwargs)|safe }}
101 | {% endif %}
102 | {% endif %}
103 | {% elif form_type == "horizontal" %}
104 |
105 | {{field.label(class="control-label " + (
106 | " col-%s-%s" % horizontal_columns[0:2]
107 | ))|safe}}
108 |
109 | {% if field.type == 'FileField' %}
110 | {{field(**kwargs)|safe}}
111 | {% else %}
112 | {% if field.errors %}
113 | {{ field(class="form-control is-invalid", **kwargs)|safe }}
114 | {% else %}
115 | {{ field(class="form-control", **kwargs)|safe }}
116 | {% endif %}
117 | {% endif %}
118 |
119 | {%- if field.errors %}
120 | {%- for error in field.errors %}
121 | {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
122 |
{{error}}
123 | {% endcall %}
124 | {%- endfor %}
125 | {%- elif field.description -%}
126 | {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
127 |
{{field.description|safe}}
128 | {% endcall %}
129 | {%- endif %}
130 | {%- else -%}
131 |
132 | {{field.label(class="control-label")|safe}}
133 | {% if field.type == 'FileField' %}
134 | {{field(**kwargs)|safe}}
135 | {% else %}
136 | {% if field.errors %}
137 | {{ field(class="form-control is-invalid", **kwargs)|safe }}
138 | {% else %}
139 | {{ field(class="form-control", **kwargs)|safe }}
140 | {% endif %}
141 | {% endif %}
142 |
143 | {%- if field.errors %}
144 | {%- for error in field.errors %}
145 |
{{error}}
146 | {%- endfor %}
147 | {%- elif field.description -%}
148 |
{{field.description|safe}}
149 | {%- endif %}
150 | {%- endif %}
151 |
152 | {% endif %}
153 | {% endmacro %}
154 |
155 | {# valid form types are "basic", "inline" and "horizontal" #}
156 | {% macro quick_form(form,
157 | action="",
158 | method="post",
159 | extra_classes=None,
160 | role="form",
161 | form_type="basic",
162 | horizontal_columns=('lg', 2, 10),
163 | enctype=None,
164 | button_map={},
165 | id="",
166 | novalidate=False) %}
167 | {#-
168 | action="" is what we want, from http://www.ietf.org/rfc/rfc2396.txt:
169 |
170 | 4.2. Same-document References
171 |
172 | A URI reference that does not contain a URI is a reference to the
173 | current document. In other words, an empty URI reference within a
174 | document is interpreted as a reference to the start of that document,
175 | and a reference containing only a fragment identifier is a reference
176 | to the identified fragment of that document. Traversal of such a
177 | reference should not result in an additional retrieval action.
178 | However, if the URI reference occurs in a context that is always
179 | intended to result in a new request, as in the case of HTML's FORM
180 | element, then an empty URI reference represents the base URI of the
181 | current document and should be replaced by that URI when transformed
182 | into a request.
183 |
184 | -#}
185 | {#- if any file fields are inside the form and enctype is automatic, adjust
186 | if file fields are found. could really use the equalto test of jinja2
187 | here, but latter is not available until 2.8
188 |
189 | warning: the code below is guaranteed to make you cry =(
190 | #}
191 | {%- set _enctype = [] %}
192 | {%- if enctype is none -%}
193 | {%- for field in form %}
194 | {%- if field.type == 'FileField' %}
195 | {#- for loops come with a fairly watertight scope, so this list-hack is
196 | used to be able to set values outside of it #}
197 | {%- set _ = _enctype.append('multipart/form-data') -%}
198 | {%- endif %}
199 | {%- endfor %}
200 | {%- else %}
201 | {% set _ = _enctype.append(enctype) %}
202 | {%- endif %}
203 |
230 | {%- endmacro %}
231 |
--------------------------------------------------------------------------------
/app/t_roles/route.py:
--------------------------------------------------------------------------------
1 | from flask import (
2 | current_app,
3 | redirect,
4 | url_for,
5 | render_template,
6 | Blueprint,
7 | request,
8 | flash,
9 | current_app,
10 | )
11 | import re
12 | import sqlalchemy as sa
13 |
14 | from pypnusershub import routes as fnauth
15 | from pypnusershub.db.models import check_and_encrypt_password
16 | from pypnusershub.db.models import cor_role_provider
17 |
18 |
19 | from app.t_roles import forms as t_rolesforms
20 | from app.models import TRoles, Bib_Organismes, CorRoles
21 | from app.utils.utils_all import strigify_dict
22 | from app.env import db
23 |
24 |
25 | URL_APPLICATION = current_app.config["URL_APPLICATION"]
26 |
27 | route = Blueprint("user", __name__)
28 |
29 |
30 | @route.route("users/list", methods=["GET"])
31 | @fnauth.check_auth(
32 | 3,
33 | )
34 | def users():
35 | """
36 | Route qui affiche la liste des utilisateurs
37 | Retourne un template avec pour paramètres :
38 | - une entête de tableau --> fLine
39 | - le nom des colonnes de la base --> line
40 | - le contenu du tableau --> table
41 | - le chemin de mise à jour --> pathU
42 | - le chemin de suppression --> pathD
43 | - le chemin d'ajout --> pathA
44 | - le chemin de la page d'information --> pathI
45 | - une clé (clé primaire dans la plupart des cas) --> key
46 | - un nom (nom de la table) pour le bouton ajout --> name
47 | - un nom de listes --> name_list
48 | - ajoute une colonne pour accéder aux infos de l'utilisateur --> see
49 | """
50 | fLine = [
51 | "Id",
52 | "Identifiant",
53 | "Nom",
54 | "Prenom",
55 | "Email",
56 | "Organisme",
57 | "Remarques",
58 | "Actif",
59 | "pass_plus",
60 | "pass_md5",
61 | ] # noqa
62 | columns = [
63 | "id_role",
64 | "identifiant",
65 | "nom_role",
66 | "prenom_role",
67 | "email",
68 | "nom_organisme",
69 | "remarques",
70 | "active",
71 | "pass_plus",
72 | "pass_md5",
73 | ] # noqa
74 | filters = [{"col": "groupe", "filter": "False"}]
75 | contents = TRoles.get_all(columns, filters, order_by="identifiant", order="asc")
76 | tab = []
77 | for data in contents:
78 | data["nom_organisme"] = (
79 | data["organisme_rel"]["nom_organisme"]
80 | if data.get("organisme_rel")
81 | else None
82 | )
83 | if data["pass_plus"] == "" or data["pass_plus"] is None:
84 | data["pass_plus"] = "Non"
85 | else:
86 | data["pass_plus"] = "Oui"
87 | if data["pass_md5"] == "" or data["pass_md5"] is None:
88 | data["pass_md5"] = "Non"
89 | else:
90 | data["pass_md5"] = "Oui"
91 | tab.append(data)
92 |
93 | return render_template(
94 | "table_database.html",
95 | fLine=fLine,
96 | line=columns,
97 | table=tab,
98 | see="True",
99 | key="id_role",
100 | pathI=URL_APPLICATION + "/user/info/",
101 | pathU=URL_APPLICATION + "/user/update/",
102 | pathD=URL_APPLICATION + "/users/delete/",
103 | pathA=URL_APPLICATION + "/user/add/new",
104 | pathZ=URL_APPLICATION + "/user/pass/",
105 | passPlusCol="True",
106 | passMd5Col="True",
107 | name="un utilisateur",
108 | name_list="Utilisateurs",
109 | )
110 |
111 |
112 | @route.route("user/add/new", methods=["GET", "POST"])
113 | @route.route("user/update/", methods=["GET", "POST"])
114 | @fnauth.check_auth(
115 | 6,
116 | )
117 | def addorupdate(id_role=None):
118 | """
119 | Route affichant un formulaire vierge ou non (selon l'url) pour ajouter ou mettre à jour un utilisateurs
120 | L'envoie du formulaire permet l'ajout ou la mise à jour de l'utilisateur dans la base
121 | Retourne un template accompagné du formulaire pré-rempli ou non selon le paramètre id_role
122 | Une fois le formulaire validé on retourne une redirection vers la liste des utilisateurs
123 | """
124 | form = t_rolesforms.Utilisateur()
125 | form.id_organisme.choices = Bib_Organismes.choixSelect(
126 | "id_organisme", "nom_organisme", order_by="nom_organisme"
127 | )
128 | form.a_groupe.choices = TRoles.choix_group("id_role", "nom_role", aucun=None)
129 |
130 | if id_role is not None:
131 | user = TRoles.get_one(id_role, as_model=True)
132 | user_as_dict = user.as_dict_full_name()
133 | # format group to prepfil the form
134 | formated_groups = [group.id_role for group in TRoles.get_user_groups(id_role)]
135 | if request.method == "GET":
136 | form = process(form, user_as_dict, formated_groups)
137 |
138 | if request.method == "POST":
139 | if form.validate_on_submit() and form.validate():
140 | groups = form.data["a_groupe"]
141 | form_user = pops(form.data)
142 | form_user["groupe"] = False
143 | form_user.pop("id_role")
144 |
145 | # if a password is set
146 | # check they are the same
147 | if form.pass_plus.data:
148 | try:
149 | (
150 | form_user["pass_plus"],
151 | form_user["pass_md5"],
152 | ) = check_and_encrypt_password(
153 | form.pass_plus.data,
154 | form.mdpconf.data,
155 | current_app.config["PASS_METHOD"] == "md5"
156 | or current_app.config["FILL_MD5_PASS"],
157 | )
158 | except Exception as exp:
159 | flash(str(exp), "error")
160 | return render_template(
161 | "user.html", form=form, title="Formulaire Utilisateur"
162 | )
163 |
164 | if id_role is not None:
165 | # HACK a l'update on remet a la main les mdp
166 | # car on les masque dans le form
167 | form_user["pass_plus"] = user.pass_plus
168 | form_user["pass_md5"] = user.pass_md5
169 | form_user["id_role"] = user.id_role
170 | new_role = TRoles.update(form_user)
171 | else:
172 | new_role = TRoles.post(form_user)
173 | # set groups
174 | if len(groups) > 0:
175 | if id_role:
176 | # first delete all groups of the user
177 | cor_role_to_delete = CorRoles.get_all(
178 | params=[{"col": "id_role_utilisateur", "filter": id_role}],
179 | as_model=True,
180 | )
181 | for cor_role in cor_role_to_delete:
182 | db.session.delete(cor_role)
183 | db.session.commit()
184 | for group in groups:
185 | # add new groups
186 | new_group = CorRoles(
187 | id_role_groupe=group, id_role_utilisateur=new_role.id_role
188 | )
189 | db.session.add(new_group)
190 | db.session.commit()
191 | return redirect(url_for("user.users"))
192 |
193 | else:
194 | flash(strigify_dict(form.errors), "error")
195 | return render_template(
196 | "user.html", form=form, title="Formulaire Utilisateur", id_role=id_role
197 | )
198 |
199 |
200 | @route.route("user/pass/", methods=["GET", "POST"])
201 | @fnauth.check_auth(
202 | 6,
203 | )
204 | def updatepass(id_role=None):
205 | """
206 | Route affichant un formulaire permettant de changer le pass des utilisateurs
207 | L'envoie du formulaire permet la mise à jour du pass de l'utilisateur dans la base
208 | Retourne un template accompagné du formulaire pré-rempli ou non selon le paramètre id_role
209 | Une fois le formulaire validé on retourne une redirection vers la liste des utilisateurs
210 | """
211 | form = t_rolesforms.UserPass()
212 | myuser = TRoles.get_one(id_role)
213 | # Build title
214 | role_fullname = buildUserFullName(myuser)
215 | title = f"Changer le mot de passe de l'utilisateur '{role_fullname}'"
216 |
217 | if request.method == "POST":
218 | if form.validate_on_submit() and form.validate():
219 | form_user = pops(form.data, False)
220 | form_user.pop("id_role")
221 | # check if passwords are the same
222 | if form.pass_plus.data:
223 | try:
224 | (
225 | form_user["pass_plus"],
226 | form_user["pass_md5"],
227 | ) = check_and_encrypt_password(
228 | form.pass_plus.data,
229 | form.mdpconf.data,
230 | current_app.config["PASS_METHOD"] == "md5"
231 | or current_app.config["FILL_MD5_PASS"],
232 | )
233 | except Exception as exp:
234 | flash({"password": [exp]}, "error")
235 | return render_template(
236 | "user_pass.html",
237 | form=form,
238 | title=title,
239 | id_role=id_role,
240 | )
241 | form_user["id_role"] = id_role
242 | TRoles.update(form_user)
243 | return redirect(url_for("user.users"))
244 | else:
245 | flash(strigify_dict(form.errors), "error")
246 |
247 | return render_template(
248 | "user_pass.html",
249 | form=form,
250 | title=title,
251 | id_role=id_role,
252 | )
253 |
254 |
255 | @route.route("users/delete/", methods=["GET", "POST"])
256 | @fnauth.check_auth(
257 | 6,
258 | )
259 | def deluser(id_role):
260 | """
261 | Route qui supprime un utilisateurs dont l'id est donné en paramètres dans l'url
262 | Retourne une redirection vers la liste d'utilisateurs
263 | """
264 |
265 | TRoles.delete(id_role)
266 | return redirect(url_for("user.users"))
267 |
268 |
269 | @route.route("user/info/", methods=["GET", "POST"])
270 | @fnauth.check_auth(6)
271 | def info(id_role):
272 | user = TRoles.get_one(id_role)
273 | organisme = (
274 | Bib_Organismes.get_one(user["id_organisme"]) if user["id_organisme"] else None
275 | )
276 | fullname = buildUserFullName(user)
277 | groups = TRoles.get_user_groups(id_role)
278 | lists = TRoles.get_user_lists(id_role)
279 | rights = TRoles.get_user_app_profils(id_role)
280 | return render_template(
281 | "info_user.html",
282 | user=user,
283 | organisme=organisme,
284 | fullname=fullname,
285 | groups=groups,
286 | lists=lists,
287 | rights=rights,
288 | pathU=URL_APPLICATION + "/user/update/",
289 | )
290 |
291 |
292 | def buildUserFullName(user):
293 | fullname = []
294 | if user["nom_role"]:
295 | fullname.append(user["nom_role"].upper())
296 | if user["prenom_role"]:
297 | fullname.append(user["prenom_role"].title())
298 | return " ".join(fullname)
299 |
300 |
301 | @route.app_template_filter()
302 | def pretty_json_key(key):
303 | return re.sub("([a-z])([A-Z])", "\g<1> \g<2>", key)
304 |
305 |
306 | def pops(form, with_group=True):
307 | """
308 | Methode qui supprime les éléments indésirables du formulaires
309 | Avec pour paramètre un formulaire
310 | """
311 | form.pop("mdpconf")
312 | form.pop("submit")
313 | form.pop("csrf_token")
314 | if with_group:
315 | form.pop("a_groupe")
316 | return form
317 |
318 |
319 | def process(form, user, groups):
320 | """
321 | Methode qui rempli le formulaire par les données de l'éléments concerné
322 | Avec pour paramètres un formulaire, un user et les groupes
323 | auxquels il appartient
324 | """
325 | form.active.process_data(user["active"])
326 | form.id_organisme.process_data(user["id_organisme"])
327 | form.nom_role.process_data(user["nom_role"])
328 | form.prenom_role.process_data(user["prenom_role"])
329 | form.email.process_data(user["email"])
330 | form.remarques.process_data(user["remarques"])
331 | form.identifiant.process_data(user["identifiant"])
332 | form.a_groupe.process_data(groups)
333 | return form
334 |
335 |
336 | @route.route("test", methods=["GET", "POST"])
337 | def test(id_role):
338 | fLine = [{"key": "test1", "label": "Test 1"}, {"key": "test2", "label": "Test 2"}]
339 |
340 | tab = [{"test1": "test1", "test2": "Test 1"}, {"test1": "test2", "test2": "Test 2"}]
341 | return render_template("generic_table.html", fLine=fLine, table=tab)
342 |
--------------------------------------------------------------------------------