├── 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 | 9 | {% else %} 10 | 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 |
7 | {{ form.hidden_tag() }} {{ form.csrf_token }} 8 |

{{ title }}

9 | 10 |
11 | {% include "alert_messages.html" %} 12 | 13 | {{ wtf.form_field(form.nom_role) }} 14 | {{ wtf.form_field(form.desc_role) }} 15 | {{ wtf.form_field(form.submit, class="form-control btn-success") }} 16 |
17 |
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 |
7 | {{ form.hidden_tag() }} {{ form.csrf_token }} 8 |

{{ title }}

9 | 10 |
11 | {% include "alert_messages.html" %} 12 | 13 | {{ wtf.form_field(form.nom_liste) }} 14 | {{ wtf.form_field(form.code_liste) }} 15 | {{ wtf.form_field(form.desc_liste) }} 16 | {{ wtf.form_field(form.submit, class="form-control btn-success") }} 17 |
18 |
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 |
7 | {{ form.hidden_tag() }} {{ form.csrf_token }} 8 |

{{ title }}

9 | 10 |
11 | {% include "alert_messages.html" %} 12 | 13 | {{ wtf.form_field(form.nom_profil) }} 14 | {{ wtf.form_field(form.code_profil) }} 15 | {{ wtf.form_field(form.desc_profil) }} 16 | {{ wtf.form_field(form.submit, class="form-control btn-success") }} 17 |
18 |
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 |
10 |

{{title}}

11 | 12 |
13 | {{ form.hidden_tag() }} 14 | {% if id_role == None %} 15 | {{ wtf.form_field(form.role) }} 16 | {% else %} 17 | {{ wtf.form_field(form.role, disabled=True) }} 18 | {% endif %} 19 | {{ wtf.form_field(form.profil) }} 20 | {{ wtf.form_field(form.submit, class="btn btn-success") }} 21 |
22 | 23 |
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 |
10 |

{{ title }}

11 | 12 | {{ form.hidden_tag() }} 13 | 14 |
15 | {% include "alert_messages.html" %} 16 | 19 | 20 | {{ wtf.form_field(form.pass_plus) }} 21 | {{ wtf.form_field(form.mdpconf) }} 22 | {{ wtf.form_field(form.submit, class="btn btn-success") }} 23 |
24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /app/templates/generic_table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% for el in columns %} 7 | 8 | {% endfor %} 9 | {% block table_head %} 10 | {% endblock %} 11 | 12 | 13 | 14 | {% for elt in data %} 15 | 16 | {% for col in columns %} 17 | 18 | {% endfor %} 19 | {% block table_td scoped%} 20 | {% endblock %} 21 | 22 | {% endfor %} 23 | 24 |
{{ el.label }}
{{ elt[col.key] }}
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 |
7 | {{ form.hidden_tag() }} {{ form.csrf_token }} 8 |

{{ title }}

9 |
10 | {% include "alert_messages.html" %} 11 | 12 | {{ wtf.form_field(form.nom_organisme) }} 13 | {{ wtf.form_field(form.adresse_organisme) }} 14 | {{ wtf.form_field(form.cp_organisme) }} 15 | {{ wtf.form_field(form.ville_organisme) }} 16 | {{ wtf.form_field(form.tel_organisme) }} 17 | {{ wtf.form_field(form.fax_organisme) }} 18 | {{ wtf.form_field(form.email_organisme) }} 19 | {{ wtf.form_field(form.url_organisme) }} 20 | {{ wtf.form_field(form.url_logo) }} 21 | {{ wtf.form_field(form.submit, class="form-control btn-success") }} 22 |
23 |
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 |
10 |

{{ title }}

11 | 12 | {{ form.hidden_tag() }} 13 | 14 |
15 | {% include "alert_messages.html" %} 16 | 17 | {{ wtf.form_field(form.active) }} 18 | {{ wtf.form_field(form.nom_role) }} 19 | {{ wtf.form_field(form.prenom_role) }} 20 | {{ wtf.form_field(form.identifiant) }} 21 | {{ wtf.form_field(form.id_organisme, id="inputGroupSelect01") }} 22 | {% if not id_role %} 23 | {{ wtf.form_field(form.pass_plus) }} 24 | {{ wtf.form_field(form.mdpconf) }} 25 | {% endif %} 26 | 27 | {{ wtf.form_field(form.a_groupe) }} 28 | {{ wtf.form_field(form.email) }} 29 | {{ wtf.form_field(form.remarques) }} 30 | {{ wtf.form_field(form.submit, class="btn btn-success") }} 31 |
32 |
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 |
7 |
8 | {{ form.hidden_tag() }} {{ form.csrf_token }} 9 |

{{ title }}

10 | 11 |
12 | {% include "alert_messages.html" %} 13 | 14 | {{ wtf.form_field(form.nom_application) }} 15 | {{ wtf.form_field(form.code_application) }} 16 | {{ wtf.form_field(form.desc_application) }} 17 | {{ wtf.form_field(form.id_parent) }} 18 | {{ wtf.form_field(form.submit, class="form-control btn-success") }} 19 | 20 | {% if id_application %} 21 |
22 |
23 | {% if profils|length > 0 %} 24 |

Liste des profils disponibles pour cette application

25 |
    26 | {% for profil in profils%} 27 |
  • {{profil.nom_profil}} ({{profil.code_profil}})
  • 28 | {% endfor %} 29 |
30 | {%else%} 31 |

Attention, aucun profil n'a été défini pour cette application


32 | {% endif %} 33 | 38 |
39 |
40 | {% endif %} 41 | 42 |
43 |
44 |
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 | 24 | 25 | 27 | 31 | 32 | {% endblock table_head %} 33 | 34 | 35 | {% block table_td %} 36 | 37 | 38 | 39 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 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 | 12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 | 21 |
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 |
13 |
Liste des utilisateurs membres de la liste
14 |
15 |
16 | {% if is_members %} 17 | 31 | {% else %} 32 |

Le groupe ne comporte aucun membre.

33 | {% endif %} 34 |
35 |
36 |
37 | 38 | 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 |
14 |
15 | Liste des profils implémentés dans l'application 16 | 17 | 20 | 21 |
22 |
23 |
24 |
25 | {% for profil in profils %} 26 | {{profil.profil_rel.nom_profil}} - 27 | {% endfor %} 28 |
29 |
30 |
31 |
32 |
33 |
34 |
Liste des utilisateurs ayant un profil dans l'application
35 |
36 |
37 | 59 |
60 |
61 |
62 | 63 | 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 |
45 |
Liste des utilisateurs membres de l'organisme "{{org['nom_organisme']}}"
46 |
47 |
48 | {% if is_users %} 49 | 57 | {% else %} 58 |

L'organisme ne comporte aucun utilisateur.

59 | {% endif %} 60 |
61 |
62 |
63 | 64 | 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 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | {% for name in fLine %} 17 | 18 | {% endfor %} 19 | 20 | 21 | 22 | {% for elt in table %} 23 | 24 | 25 | {% for name in data %} 26 | 27 | {% endfor %} 28 | 29 | {% endfor %} 30 | 31 |
Profils disponibles non utilisables dans l'application
#{{name}}
{{elt[name]}}
32 |
33 | 34 |
35 |

36 | 39 |

40 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | {% for name in fLine %} 52 | 53 | {% endfor %} 54 | 55 | 56 | 57 | {% for elt in table2 %} 58 | 59 | 60 | {% for name in data %} 61 | 62 | {% endfor %} 63 | 64 | {%endfor%} 65 | 66 |
Profils utilisables pour l'application
#{{name}}
{{elt[name]}}
67 |
68 | 69 |
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 |
14 |
15 | 16 | 17 | 18 | 19 | {% for name in fLine %} 20 | 21 | {% endfor %} 22 | 23 | 24 | 25 | {% for elt in table %} 26 | 27 | 28 | {% for name in data %} 29 | {% set groups = elt[group] %} 30 | {% if groups == 'True' %} 31 | 32 | {% else %} 33 | 34 | {% endif%} 35 | {% endfor %} 36 | 37 | {% endfor %} 38 | 39 |
#{{name}}
{{elt[name]}}{{elt[name]}}
40 |
41 |
42 |




43 | {% if app == 'True'%} 44 | 47 |

48 | 51 | {% else %} 52 | 55 |

56 | 59 | {% endif%} 60 |
61 | 62 |
63 | 64 | 65 | 66 | 67 | {% for name in fLine %} 68 | 69 | {% endfor %} 70 | {% if app == 'True'%} 71 | 72 | {% endif%} 73 | 74 | 75 | 76 | {% for elt in table2 %} 77 | 78 | 79 | {% for name in data %} 80 | {% set groups = elt[group] %} 81 | {% if groups == 'True' %} 82 | 83 | {% else %} 84 | 85 | {% endif%} 86 | {% endfor %} 87 | {% if app == 'True'%} 88 | 99 | {% endif%} 100 | 101 | {%endfor%} 102 | 103 |
#{{name}}Droit
{{elt[name]}}{{elt[name]}} 89 | 98 |
104 |
105 | 106 | {% if app == 'True'%} 107 | 108 | {% else %} 109 | 110 | {% endif%} 111 | 112 |
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 |
15 |
Liste des utilisateurs membres du groupe 16 | 17 | 18 | 19 | 22 | 23 |
24 |
25 |
26 | {% if is_members %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% for user in members %} 36 | 37 | 51 | 58 | 59 | {% endfor %} 60 | 61 |
NomEmail
38 | {% set is_group = user.groupe %} 39 | {% if is_group %} 40 | 41 | {{user.nom_role}} 42 | {% else %} 43 | 44 | {%if user.prenom_role %} 45 | {{user.prenom_role}} 46 | {% endif %} 47 | {{user.nom_role}} 48 | 49 | {% endif %} 50 | 52 | {% if user.email %} 53 | {{user.email}} 54 | {% else %} 55 |   56 | {% endif %} 57 |
62 | {% else %} 63 |

Le groupe ne comporte aucun membre.

64 | {% endif %} 65 |
66 |
67 |
68 |
69 |
70 |
Listes auxquelles le groupe appartient
71 |
72 |
73 | {% if is_lists %} 74 | 86 | {% else %} 87 |

Le groupe n'appartient à aucune liste.

88 | {% endif %} 89 |
90 |
91 |
92 |
93 |
94 |
Profils du groupe dans les applications
95 |
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 | 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 | 29 | {% endfor %} 30 | {% if passPlusCol == 'True' or passMd5Col %} 31 | 34 | {% endif %} 35 | {% if otherCol == 'True' %} 36 | 39 | {% endif %} 40 | {% if tag_orga == 'True' %} 41 | 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 | 60 | {% else %} 61 | 62 | {% endif %} 63 | {% endfor %} 64 | 65 | {% if passPlusCol == 'True' or passMd5Col == 'True' %} 66 | 80 | {% endif %} 81 | 82 | {% if otherCol == 'True' %} 83 | 90 | {% endif %} 91 | 92 | {% if permissions == 'True' %} 93 | 100 | {% endif %} 101 | 102 | {% if see == 'True' %} 103 | 110 | {% endif %} 111 | 112 | 123 | 132 | 133 | {% endfor %} 134 | 135 |
{{ name }} 32 | 33 | 37 | 38 | {{ Organismes }}
{{ elt[name] }}{{ elt[name] }} 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 | 78 | 79 | 84 | 85 | 88 | 89 | 94 | 95 | 98 | 99 | 104 | 105 | 108 | 109 | 113 | {% if key2 %} 114 | 115 | {% else %} 116 | 117 | {% endif %} 118 | 121 | 122 | 124 | {% if key2 %} 125 | 131 |
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 | '"; 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 |
86 |
Liste des groupes auxquels appartient l'utilisateur
87 |
88 |
89 | {%if is_group %} 90 | 102 | {% else %} 103 |

L'utilisateur n'appartient à aucun groupe.

104 | {% endif %} 105 |
106 |
107 |
108 |
109 |
110 |
Liste des listes auxquelles appartient l'utilisateur
111 |
112 |
113 | {%if is_list %} 114 | 126 | {% else %} 127 |

L'utilisateur n'appartient à aucune liste.

128 | {% endif %} 129 |
130 |
131 |
132 |
133 |
134 |
Liste des applications pour lesquelles l'utilisateur dispose d'un profil
135 |
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 | 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 %}
{% endif %} 17 |
20 | {% endif %} 21 | {{caller()}} 22 | 23 | {% if form_type == "horizontal" %} 24 | {% if add_group %}
{% endif %} 25 |
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 | 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 | 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 |
217 | {{ form.hidden_tag() }} 218 | {{ form_errors(form, hiddens='only') }} 219 | 220 | {%- for field in form %} 221 | {% if not bootstrap_is_hidden_field(field) -%} 222 | {{ form_field(field, 223 | form_type=form_type, 224 | horizontal_columns=horizontal_columns, 225 | button_map=button_map) }} 226 | {%- endif %} 227 | {%- endfor %} 228 | 229 |
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 | --------------------------------------------------------------------------------