Commit 96ef779b1109faa864be5c47ddc25b66eaed7bd6
Exists in
master
and in
4 other branches
Irap db Integration
Feed database with charges from irap csv file
Showing
14 changed files
with
330 additions
and
121 deletions
Show diff stats
CHANGELOG.md
... | ... | @@ -24,6 +24,15 @@ or major refactoring improvments. |
24 | 24 | |
25 | 25 | ## Unreleased |
26 | 26 | |
27 | +## [0.3.pre-1] - 2021-04-16 - Irap db integration | |
28 | +### New | |
29 | +Irap db feed from csv file | |
30 | +Versionize static assets | |
31 | +User management commands | |
32 | + | |
33 | +### Changed | |
34 | +Tiny chart enhancements | |
35 | + | |
27 | 36 | ## [0.2.1] - 2021-04-15 - Password Encryption |
28 | 37 | ### New |
29 | 38 | Password encrypted in database | ... | ... |
INSTALL.md
... | ... | @@ -12,7 +12,7 @@ |
12 | 12 | export GIT_SSL_NO_VERIFY=1 |
13 | 13 | git clone https://gitlab.irap.omp.eu/PDC-IRAP/pdc-web.git |
14 | 14 | cd pdc-web |
15 | - git checkout dev # éventuellement travailler avec la branche la plus à jour | |
15 | + git checkout dev # pour éventuellement travailler avec la branche la plus à jour | |
16 | 16 | |
17 | 17 | ### Installer l'environment python |
18 | 18 | |
... | ... | @@ -24,57 +24,66 @@ |
24 | 24 | ### Configurer l'application |
25 | 25 | |
26 | 26 | Les fichiers de configuration fournis dans le répertoire ./resources sont à copier à la racine du projet, |
27 | -mais peuvent être laissés tels quels pour un premier test aprés installation. | |
27 | +mais peuvent être laissés tels quels pour un premier test après installation. | |
28 | 28 | |
29 | -Il est bon d'y jeter un oeil, les commentaires sont là pour aider. | |
29 | +Il est bon d'y jeter un oeil, les commentaires sont là pour vous aider. | |
30 | +(mais en anglais comme tout le code source du projet) | |
30 | 31 | |
31 | 32 | # D'abord les accès base de donnée |
32 | 33 | # |
33 | - cp resources/db_config.py . | |
34 | + cp ./resources/db_config.py ./ | |
34 | 35 | $(EDITOR) db_config.py |
35 | 36 | |
36 | 37 | # Puis le fichier pour l'application elle même |
37 | 38 | # |
38 | - cp resources/pdc_config.py . | |
39 | + cp ./resources/pdc_config.py ./ | |
39 | 40 | $(EDITOR) pdc_config.py |
40 | 41 | |
41 | - # Enfin le contrôle de la commande flask | |
42 | - # également chargé par l'application grace à l'instruction: | |
42 | + # Enfin le contrôle de la commande flask est assuré par son propre fichier de configuration | |
43 | + # Celui ci est également chargé par l'application grace à l'instruction: | |
43 | 44 | # app/__init__.py: load_dotenv(env_file, verbose=True) |
44 | 45 | # |
45 | - cp resources/flaskenv .flaskenv # ! noter le '.' devant le fichier destination | |
46 | + cp ./resources/flaskenv ./.flaskenv # ! noter le '.' devant le fichier destination | |
46 | 47 | $(EDITOR) .flaskenv |
47 | 48 | |
48 | 49 | ### Créer la base de données |
49 | 50 | |
50 | -Dans un premier temps pour tester l'installation, on peut s'appuyer sur une base déjà disponible. | |
51 | +Dans un premier temps, pour tester l'installation, on peut s'appuyer sur une base déjà disponible. | |
51 | 52 | |
52 | 53 | Il s'agit de l'import d'une base de type Lesia dont les noms d'agents, de projets, de métiers et de services ont été |
53 | -réécrit avec des chantiers du batiment. Cela afin de ne pas diffuser des données réelles du Lesia et permettre la | |
54 | -diffusion de ces données de test avec le projet plan-de-charge. | |
54 | +réécrit avec des chantiers du batiment. Cela fut fait afin de ne pas diffuser des données réelles du Lesia et permettre | |
55 | +la diffusion de ces données de test avec le projet plan-de-charge. | |
55 | 56 | |
56 | 57 | cp resources/lesia-btp.sqlite ./pdc-dev.db |
57 | 58 | |
58 | -Vérifier que ce chemin correspond avec celui configuré dans le fichier `db_config.py` | |
59 | +Vérifier que ce chemin correspond avec celui configuré dans le fichier `db_config.py` pour la | |
60 | +variable `sqlalchemy_devdb_uri`. | |
61 | +C'est le cas dans le fichier initial pour une variable `FLASK_ENV` positionnée à 'development'. | |
62 | + | |
63 | +Pour un usage plus avancé, voyez l'outil en ligne de commande fourni avec l'application. | |
64 | +(sinon, passez directement à la section suivante ) | |
59 | 65 | |
60 | -Pour un usage plus avancé, il y a l'outil de ligne de commande fourni avec l'application. | |
61 | 66 | |
62 | 67 | # Créer la structure de la base |
63 | 68 | # |
64 | 69 | flask pdc_db create_db |
65 | 70 | |
66 | - # Il est aussi possible d'importer une base de type Lesia | |
71 | + # Il est possible d'importer une base de type Lesia | |
67 | 72 | # cela suppose de disposer d'une telle base, et de l'avoir configurée dans db_config.py |
68 | 73 | # |
69 | 74 | flask pdc_db feed_from_lesia |
70 | 75 | |
71 | 76 | # Voire de l'anonymiser en changeant |
72 | - # - les noms de projet | |
73 | - # - les noms de service | |
77 | + # - les noms de projets | |
78 | + # - les noms de services | |
74 | 79 | # - les noms de fonctions |
75 | 80 | # |
76 | 81 | flask pdc_db fake_lesia_names |
77 | 82 | |
83 | + # D'autre commandes d'importation sont disponibles: | |
84 | + # | |
85 | + flask pdc_db feed_from_irap --csv-file 2021_03_30_PDC_v0-1.csv | |
86 | + | |
78 | 87 | # Plus d'info sur les outils en ligne de commande: |
79 | 88 | # |
80 | 89 | flask --help |
... | ... | @@ -84,7 +93,7 @@ Pour un usage plus avancé, il y a l'outil de ligne de commande fourni avec l'ap |
84 | 93 | ## Jouer les tests et exécuter un serveur local |
85 | 94 | |
86 | 95 | pip install -r requirements-tests.txt |
87 | - cp resources/lesia-btp.sqlite ./pdc-test.db # ou le chemin configuré dans `db_config.py` | |
96 | + cp resources/lesia-btp.sqlite ./pdc-test.db # ou le chemin configuré dans `db_config.py` pour sqlalchemy_testdb_uri | |
88 | 97 | PYTHONPATH=. pytest |
89 | 98 | |
90 | 99 | # éventuellement voir le taux de couverture |
... | ... | @@ -131,6 +140,8 @@ Enfin, ouvrir un serveur sur localhost:5000 et y accéder avec son navigateur. |
131 | 140 | |
132 | 141 | ## Mise à jour |
133 | 142 | |
143 | +### git pull | |
144 | + | |
134 | 145 | ### git autodeploy |
135 | 146 | |
136 | 147 | Les fichiers concernés: |
... | ... | @@ -150,7 +161,16 @@ La procédure: |
150 | 161 | |
151 | 162 | git push to repo |
152 | 163 | |
164 | +## Gestion des utilisateurs | |
153 | 165 | |
166 | +La table `users` stocke les utilisateur qui se connectent à l'applicationa avec leur rôle et les droits associés. | |
167 | +Un ensemble de commandes permet de les gérer: | |
168 | + | |
169 | + flask pdc_db user_show_all # liste existante | |
170 | + flask pdc_db user_add # ajouter un nouveau login | |
171 | + flask pdc_db user_update # modifier un login existant | |
172 | + flask pdc_db user_delete # effacer un login existant | |
173 | + flask pdc_db show_roles # lister les rôles disponibles | |
154 | 174 | |
155 | 175 | ## Intégration Pycharm |
156 | 176 | |
... | ... | @@ -168,10 +188,15 @@ Dans le menu 'Edit Configurations', changer les champs: |
168 | 188 | - et dans le champs 'Parameters' choisir 'run' |
169 | 189 | |
170 | 190 | |
171 | -Normalement, cette configuration permet de lire les variables positionées dans le fichier .flaskenv | |
191 | +Cette configuration permet de lire les variables positionées dans le fichier .flaskenv | |
172 | 192 | |
173 | 193 | FLASK_ENV=development |
174 | 194 | FLASK_APP=pdc_web |
175 | 195 | |
176 | 196 | |
177 | 197 | Ainsi fait, exécutez votre projet depuis pycharm et essayez sur un navigateur à l'adresse localhost:5000. |
198 | + | |
199 | +## Troubleshooting | |
200 | + | |
201 | +* Q: parfois le module Flask-Migrate n'est pas correctement chargé aprés son installation. | |
202 | +* R: simplement recharger l'environnement virtuel avec `source venv/bin/activate` | ... | ... |
VERSION.txt
app/auth/templates/login.html
... | ... | @@ -2,15 +2,15 @@ |
2 | 2 | <html lang="fr"> |
3 | 3 | <head> |
4 | 4 | {% include 'heads.html' %} |
5 | - <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" type="text/css"/> | |
6 | - <link href="{{ url_for('auth.static', filename='signin.css') }}" rel="stylesheet" type="text/css"/> | |
5 | + <link href="{{ url_for('static', filename='css/style.css', version=config.VERSION) }}" rel="stylesheet" type="text/css"/> | |
6 | + <link href="{{ url_for('auth.static', filename='signin.css', version=config.VERSION) }}" rel="stylesheet" type="text/css"/> | |
7 | 7 | </head> |
8 | 8 | <body class="text-center"> |
9 | 9 | <form action="{{ url_for('auth.login')}}" class="form-signin" method="POST"> |
10 | 10 | {% include 'flash-messages.html' %} |
11 | 11 | |
12 | 12 | <a href="{{url_for('main.index')}}"> |
13 | - <img class="mb-4 {{config.PDC_SITE_CLASS}}" height="72" src="{{ url_for('static', filename='img/pdc.png') }}" width="72"> | |
13 | + <img class="mb-4 {{config.PDC_SITE_CLASS}}" height="72" src="{{ url_for('static', filename='img/pdc.png', version=config.VERSION) }}" width="72"> | |
14 | 14 | </a> |
15 | 15 | <h1 class="h3 font-weight-normal mb-3">{{config.PDC_APP_NAME}} - {{config.PDC_SITE_NAME}}</h1> |
16 | 16 | <h1 class="h3 font-weight-normal mb-3">Connectez vous</h1> | ... | ... |
app/commands/commands.py
... | ... | @@ -6,77 +6,144 @@ import random |
6 | 6 | |
7 | 7 | from flask import current_app |
8 | 8 | from sqlalchemy.exc import OperationalError, IntegrityError |
9 | +from sqlalchemy.orm.exc import NoResultFound | |
9 | 10 | from sqlalchemy.sql import func |
10 | 11 | from sqlalchemy.ext.automap import automap_base |
11 | 12 | from sqlalchemy.orm import Session |
12 | 13 | from sqlalchemy import create_engine |
13 | 14 | |
14 | 15 | from app.models import db, Agent, Service, Project, Capacity, Period, Charge |
15 | -from app.auth.models import User, _nameToRole | |
16 | +from app.auth.models import User, _nameToRole, _roleToName | |
16 | 17 | |
17 | 18 | from . import bp |
18 | 19 | |
19 | 20 | |
20 | -@bp.cli.command("fake_lesia_names") | |
21 | -def fake_lesia_names(): | |
21 | +@bp.cli.command('create_db') | |
22 | +def create_db(): | |
22 | 23 | """ |
23 | - Extract fake name from resources files to change names in db. | |
24 | - Mainly after a lesia import, for confidential reasons | |
24 | + Create the database structure. Database should be empty. | |
25 | 25 | |
26 | - Changes nams in tables: | |
26 | + configure the proper database uri in the db_config.py file. | |
27 | + """ | |
28 | + db.create_all() | |
29 | + admin = User(email='admin@nowhere.org', name='admin', login='admin', role='admin') | |
30 | + admin.set_password('admin') | |
31 | + sqlite_uri = db.engine.url.__str__() if 'sqlite' in db.engine.url.__str__() else None | |
32 | + try: | |
33 | + db.session.add(admin) | |
34 | + db.session.commit() | |
35 | + except IntegrityError: | |
36 | + current_app.logger.error("User admin already exists, database should be empty at create") | |
37 | + if sqlite_uri: | |
38 | + current_app.logger.error("see " + sqlite_uri) | |
39 | + sys.exit(-1) | |
27 | 40 | |
28 | - - services | |
29 | - - capacities | |
30 | - - projects | |
41 | + if sqlite_uri: | |
42 | + current_app.logger.info("Created sqlite db: " + sqlite_uri) | |
43 | + | |
44 | + | |
45 | +@bp.cli.command("feed_from_irap") | |
46 | +@click.option('--csv-file', '-f', 'csv_file_name', help="the csv file path to feed from") | |
47 | +def feed_from_irap(csv_file_name): | |
48 | + """ | |
49 | + Use an Irap csv charges files and feed db with | |
50 | + | |
51 | + :param csv_file_name: | |
31 | 52 | :return: |
32 | 53 | """ |
33 | 54 | |
34 | - current_app.logger.info("Faking names from resources files") | |
35 | - # get resources files | |
55 | + rows = [] | |
56 | + | |
57 | + with open(csv_file_name, newline='') as csvfile: | |
58 | + csvreader = csv.DictReader(csvfile, delimiter=',', quotechar='"') | |
59 | + for row in csvreader: | |
60 | + # print('\n'.join(row.keys())) | |
61 | + # break | |
62 | + # Remove any leading/trailing spaces | |
63 | + row = {k: v.strip() for k, v in row.items()} | |
64 | + rows.append(row) | |
65 | + | |
66 | + firstname_key = 'NOM' | |
67 | + secondname_key = 'prénom' | |
68 | + project_key = 'PROJETS' | |
69 | + service_key = 'Groupe métier' | |
70 | + # typology_title = 'TYPOLOGIE' | |
71 | + # thematic_title = 'thématique' | |
72 | + | |
73 | + # Get the columns values | |
36 | 74 | # |
37 | - # 1- projects | |
75 | + projects = [r[project_key] for r in rows] | |
76 | + projects = sorted(set(projects)) | |
77 | + agents = [(r[firstname_key], r[secondname_key].strip()) for r in rows] | |
78 | + agents = sorted(set(agents)) | |
79 | + services = [r[service_key] for r in rows] | |
80 | + services = sorted(set(services)) | |
81 | + | |
82 | + # thematics = [r[thematic_title] for r in rows] | |
83 | + # thematics = sorted(set(thematics)) | |
84 | + # typologies = [r[typology_title] for r in rows] | |
85 | + # typologies = sorted(set(typologies)) | |
86 | + | |
87 | + # Feed agents from column | |
38 | 88 | # |
39 | - fake_projects_file = os.path.join(current_app.config['PDC_RESOURCES_DIR'], 'fake-db-names', 'fake-projects.txt') | |
40 | - with open(fake_projects_file, newline='') as csvfile: | |
41 | - spamreader = csv.reader(csvfile, delimiter=';', quotechar='|') | |
42 | - fake_projects_names = [', '.join(row) for row in spamreader] | |
43 | - fake_projects_names_iterator = iter(fake_projects_names) | |
89 | + for a in agents: | |
90 | + n_a = Agent(firstname=a[0], secondname=a[1]) | |
91 | + db.session.add(n_a) | |
92 | + db.session.commit() | |
44 | 93 | |
45 | - # 2- functions/capacities | |
94 | + # Feed projects from column | |
46 | 95 | # |
47 | - fake_capacities_file = os.path.join(current_app.config['PDC_RESOURCES_DIR'], 'fake-db-names', | |
48 | - 'fake-capacities.txt') | |
49 | - with open(fake_capacities_file, newline='') as csvfile: | |
50 | - spamreader = csv.reader(csvfile, delimiter=';', quotechar='|') | |
51 | - fake_capacities_names = [row for [row] in spamreader] | |
52 | - fake_capacities_names_iterator = iter(fake_capacities_names) | |
96 | + for p in projects: | |
97 | + n_p = Project(name=p) | |
98 | + db.session.add(n_p) | |
99 | + db.session.commit() | |
53 | 100 | |
54 | - # 3- services | |
101 | + # Feed services from column | |
55 | 102 | # |
56 | - fake_services_file = os.path.join(current_app.config['PDC_RESOURCES_DIR'], 'fake-db-names', | |
57 | - 'fake-services.txt') | |
58 | - with open(fake_services_file, newline='') as csvfile: | |
59 | - spamreader = csv.reader(csvfile, delimiter=';', quotechar='|') | |
60 | - fake_services_names = [row for row in spamreader] | |
61 | - fake_services_names_iterator = iter(fake_services_names) | |
103 | + for s in services: | |
104 | + n_s = Service(name=s) | |
105 | + db.session.add(n_s) | |
106 | + db.session.commit() | |
62 | 107 | |
63 | - # Skip columns names | |
108 | + # Feed periods names | |
109 | + # Todo: are statically built, | |
110 | + # should come from year column name. | |
64 | 111 | # |
65 | - next(fake_projects_names_iterator) | |
66 | - next(fake_capacities_names_iterator) | |
67 | - next(fake_services_names_iterator) | |
68 | - | |
69 | - for s in Service.query.all(): | |
70 | - next_service = next(fake_services_names_iterator) | |
71 | - s.name = next_service[0] | |
72 | - s.abbr = next_service[1] | |
73 | - | |
74 | - for p in Project.query.all(): | |
75 | - p.name = next(fake_projects_names_iterator) | |
112 | + for p in range(2011, 2030): | |
113 | + n_p = Period(name=f"{p}") | |
114 | + db.session.add(n_p) | |
115 | + db.session.commit() | |
76 | 116 | |
77 | - for c in Capacity.query.all(): | |
78 | - c.name = next(fake_capacities_names_iterator) | |
117 | + # Add one default capacity | |
118 | + db.session.add(Capacity(name="Travailleur")) | |
119 | + db.session.commit() | |
79 | 120 | |
121 | + # Now feed the charges. | |
122 | + # | |
123 | + # At least one for each csv row | |
124 | + # At most one for each year | |
125 | + # | |
126 | + for r in rows: | |
127 | + p = Project.query.filter(Project.name == r[project_key]).one() | |
128 | + a = Agent.query.filter(Agent.firstname == r[firstname_key], Agent.secondname == r[secondname_key]).one() | |
129 | + s = Service.query.filter(Service.name == r[service_key]).one() | |
130 | + c = Capacity.query.first() | |
131 | + for period_name in range(2011, 2030): | |
132 | + t = Period.query.filter(Period.name == period_name).one() | |
133 | + charge = r[f"{period_name}"] | |
134 | + try: | |
135 | + charge = int(100 * float(charge)) | |
136 | + except ValueError: | |
137 | + charge = 0 | |
138 | + if charge == 0: | |
139 | + continue | |
140 | + n_c = Charge(agent_id=a.id, | |
141 | + project_id=p.id, | |
142 | + service_id=s.id, | |
143 | + capacity_id=c.id, | |
144 | + period_id=t.id, | |
145 | + charge_rate=charge) | |
146 | + db.session.add(n_c) | |
80 | 147 | db.session.commit() |
81 | 148 | |
82 | 149 | |
... | ... | @@ -133,6 +200,69 @@ def feed_from_lesia(): |
133 | 200 | db.session.commit() |
134 | 201 | |
135 | 202 | |
203 | +@bp.cli.command("fake_lesia_names") | |
204 | +def fake_lesia_names(): | |
205 | + """ | |
206 | + Extract fake name from resources files to change names in db. | |
207 | + Mainly after a lesia import, for confidential reasons | |
208 | + | |
209 | + Changes nams in tables: | |
210 | + | |
211 | + - services | |
212 | + - capacities | |
213 | + - projects | |
214 | + :return: | |
215 | + """ | |
216 | + | |
217 | + current_app.logger.info("Faking names from resources files") | |
218 | + # get resources files | |
219 | + # | |
220 | + # 1- projects | |
221 | + # | |
222 | + fake_projects_file = os.path.join(current_app.config['PDC_RESOURCES_DIR'], 'fake-db-names', 'fake-projects.txt') | |
223 | + with open(fake_projects_file, newline='') as csvfile: | |
224 | + spamreader = csv.reader(csvfile, delimiter=';', quotechar='|') | |
225 | + fake_projects_names = [', '.join(row) for row in spamreader] | |
226 | + fake_projects_names_iterator = iter(fake_projects_names) | |
227 | + | |
228 | + # 2- functions/capacities | |
229 | + # | |
230 | + fake_capacities_file = os.path.join(current_app.config['PDC_RESOURCES_DIR'], 'fake-db-names', | |
231 | + 'fake-capacities.txt') | |
232 | + with open(fake_capacities_file, newline='') as csvfile: | |
233 | + spamreader = csv.reader(csvfile, delimiter=';', quotechar='|') | |
234 | + fake_capacities_names = [row for [row] in spamreader] | |
235 | + fake_capacities_names_iterator = iter(fake_capacities_names) | |
236 | + | |
237 | + # 3- services | |
238 | + # | |
239 | + fake_services_file = os.path.join(current_app.config['PDC_RESOURCES_DIR'], 'fake-db-names', | |
240 | + 'fake-services.txt') | |
241 | + with open(fake_services_file, newline='') as csvfile: | |
242 | + spamreader = csv.reader(csvfile, delimiter=';', quotechar='|') | |
243 | + fake_services_names = [row for row in spamreader] | |
244 | + fake_services_names_iterator = iter(fake_services_names) | |
245 | + | |
246 | + # Skip columns names | |
247 | + # | |
248 | + next(fake_projects_names_iterator) | |
249 | + next(fake_capacities_names_iterator) | |
250 | + next(fake_services_names_iterator) | |
251 | + | |
252 | + for s in Service.query.all(): | |
253 | + next_service = next(fake_services_names_iterator) | |
254 | + s.name = next_service[0] | |
255 | + s.abbr = next_service[1] | |
256 | + | |
257 | + for p in Project.query.all(): | |
258 | + p.name = next(fake_projects_names_iterator) | |
259 | + | |
260 | + for c in Capacity.query.all(): | |
261 | + c.name = next(fake_capacities_names_iterator) | |
262 | + | |
263 | + db.session.commit() | |
264 | + | |
265 | + | |
136 | 266 | @bp.cli.command("feed_periods") |
137 | 267 | @click.option('--begin-year', '-b', 'begin_year', default=2005, help="the start year to begin periods with") |
138 | 268 | @click.option('--end-year', '-e', 'end_year', default=2025, help="the last year to end periods with") |
... | ... | @@ -182,39 +312,6 @@ def feed_random_charges(agent): |
182 | 312 | db.session.commit() |
183 | 313 | |
184 | 314 | |
185 | -@bp.cli.command('user_delete') | |
186 | -@click.argument('user_id') | |
187 | -def user_delete(user_id): | |
188 | - """Delete the user by given id (see user_show_all").""" | |
189 | - user = User.query.get(user_id) | |
190 | - db.session.delete(user) | |
191 | - db.session.commit() | |
192 | - | |
193 | - | |
194 | -@bp.cli.command('create_db') | |
195 | -def create_db(): | |
196 | - """ | |
197 | - Create the database structure. Database should be empty. | |
198 | - | |
199 | - configure the proper database uri in the db_config.py file. | |
200 | - """ | |
201 | - db.create_all() | |
202 | - admin = User(email='admin@nowhere.org', name='admin', login='admin', role='admin') | |
203 | - admin.set_password('admin') | |
204 | - sqlite_uri = db.engine.url.__str__() if 'sqlite' in db.engine.url.__str__() else None | |
205 | - try: | |
206 | - db.session.add(admin) | |
207 | - db.session.commit() | |
208 | - except IntegrityError: | |
209 | - current_app.logger.error("User admin already exists, database should be empty at create") | |
210 | - if sqlite_uri: | |
211 | - current_app.logger.error("see " + sqlite_uri) | |
212 | - sys.exit(-1) | |
213 | - | |
214 | - if sqlite_uri: | |
215 | - current_app.logger.info("Created sqlite db: " + sqlite_uri) | |
216 | - | |
217 | - | |
218 | 315 | @bp.cli.command('user_add') |
219 | 316 | @click.argument('email') |
220 | 317 | @click.argument('name') |
... | ... | @@ -233,6 +330,44 @@ def user_add(email, name, login, password, role): |
233 | 330 | current_app.logger.info(f"added {name}") |
234 | 331 | |
235 | 332 | |
333 | +@bp.cli.command('user_update') | |
334 | +@click.option('--name', '-n', 'name', default=None, help="the name to set for that user") | |
335 | +@click.option('--role', '-r', 'role', default=None, help="the role to set for that user") | |
336 | +@click.option('--email', '-e', 'email', default=None, help="the email to set for that user") | |
337 | +@click.option('--password', '-p', 'password', default=None, help="the password to set for that user") | |
338 | +@click.argument('user_id') | |
339 | +def user_update(user_id, name, role, email, password): | |
340 | + """Update the user by given id and given parameters.""" | |
341 | + user = User.query.get(user_id) | |
342 | + if not user: | |
343 | + current_app.logger.error(f"such user_id doesnt exists {user_id}") | |
344 | + return | |
345 | + if name: | |
346 | + user.name = name | |
347 | + print(f"User --{user.name}-- name updated to {user.name}") | |
348 | + if role: | |
349 | + user.set_role(role) | |
350 | + print(f"User --{user.name}-- role updated to {_roleToName[user.role]}") | |
351 | + if email: | |
352 | + user.email=email | |
353 | + print(f"User --{user.name}-- email updated to {user.email}") | |
354 | + if password: | |
355 | + print(f"User --{user.name}-- password updated") | |
356 | + user.set_password(password) | |
357 | + if not ( name or role or email or password): | |
358 | + print(f"No update for user --{user.name}--") | |
359 | + db.session.commit() | |
360 | + | |
361 | + | |
362 | +@bp.cli.command('user_delete') | |
363 | +@click.argument('user_id') | |
364 | +def user_delete(user_id): | |
365 | + """Delete the user by given id (see user_show_all").""" | |
366 | + user = User.query.get(user_id) | |
367 | + db.session.delete(user) | |
368 | + db.session.commit() | |
369 | + | |
370 | + | |
236 | 371 | @bp.cli.command('show_roles') |
237 | 372 | def show_roles(): |
238 | 373 | """ List all available roles for a user""" |
... | ... | @@ -242,14 +377,13 @@ def show_roles(): |
242 | 377 | @bp.cli.command('user_show_all') |
243 | 378 | def user_show_all(): |
244 | 379 | """ Show all users in db.""" |
245 | - print("{:<5} {:<15} {:<15} {:<15} {:<15}".format('id', 'name', 'login', 'passwd', 'email')) | |
246 | - print("{:<5} {:<15} {:<15} {:<15} {:<15}".format('-' * 5, '-' * 15, '-' * 15, '-' * 15, '-' * 15)) | |
380 | + print("{:<5} {:<15} {:<15} {:<15}".format('id', 'name', 'login', 'email')) | |
381 | + print("{:<5} {:<15} {:<15} {:<15}".format('-' * 5, '-' * 15, '-' * 15, '-' * 15)) | |
247 | 382 | for user in User.query.all(): |
248 | 383 | print(user.login) |
249 | - print("{:<5} {:<15} {:<15} {:<15} {:<15}".format( | |
384 | + print("{:<5} {:<15} {:<15} {:<15}".format( | |
250 | 385 | user.id, |
251 | 386 | user.name, |
252 | 387 | user.login, |
253 | - user.password, | |
254 | 388 | user.email |
255 | 389 | )) | ... | ... |
app/db_mgr.py
... | ... | @@ -3,36 +3,38 @@ from app.models import db |
3 | 3 | |
4 | 4 | def projects(): |
5 | 5 | """ |
6 | - Build the list of all agents, with their charges summed up in one total | |
6 | + Build the list of all agents, with their charges for the current period | |
7 | 7 | :return: |
8 | 8 | """ |
9 | + current_period_id = get_current_period() | |
9 | 10 | sql_txt = """ |
10 | 11 | select p.id, p.name, sum(tc.charge_rate) as total_charge |
11 | 12 | from project as p left join |
12 | - ( select c.project_id, c.charge_rate from charge c ) | |
13 | + ( select c.project_id, c.charge_rate from charge c where c.period_id = {}) | |
13 | 14 | tc |
14 | 15 | on p.id = tc.project_id |
15 | 16 | group by p.id |
16 | 17 | order by total_charge desc; |
17 | - """ | |
18 | + """.format(current_period_id) | |
18 | 19 | all_projects = db.session.execute(sql_txt).fetchall() |
19 | 20 | return all_projects |
20 | 21 | |
21 | 22 | |
22 | 23 | def agents(): |
23 | 24 | """ |
24 | - Build the list of all agents, with their charges summed up in one total | |
25 | + Build the list of all agents, with their charges for the current period | |
25 | 26 | :return: |
26 | 27 | """ |
28 | + current_period_id = get_current_period() | |
27 | 29 | sql_txt = """ |
28 | 30 | select a.id, a.firstname, a.secondname, sum(tc.charge_rate) as total_charge |
29 | 31 | from agent as a left join |
30 | - ( select c.agent_id, c.charge_rate from charge c ) | |
32 | + ( select c.agent_id, c.charge_rate from charge c where c.period_id = {}) | |
31 | 33 | tc |
32 | 34 | on a.id = tc.agent_id |
33 | 35 | group by a.id |
34 | 36 | order by total_charge desc; |
35 | - """ | |
37 | + """.format(current_period_id) | |
36 | 38 | all_agents = db.session.execute(sql_txt).fetchall() |
37 | 39 | |
38 | 40 | return all_agents |
... | ... | @@ -198,3 +200,11 @@ def charges_by_agent(agent_id): |
198 | 200 | else: |
199 | 201 | all_charges.append([p, 0]) |
200 | 202 | return all_charges |
203 | + | |
204 | + | |
205 | +def get_current_period(): | |
206 | + """ | |
207 | + :return: the id of the period of current day | |
208 | + """ | |
209 | + # TODO: request on dates as soon as periods are dated | |
210 | + return 14 | ... | ... |
app/main/static/js/charges.js
... | ... | @@ -302,6 +302,9 @@ function build_chart(div_selector, data_url, entity_name, category_type) { |
302 | 302 | var categories_total_charge = {} |
303 | 303 | data.forEach(function (d) { |
304 | 304 | Object.keys(d).forEach(function (k) { |
305 | + if (k === 'period') { | |
306 | + return; | |
307 | + } | |
305 | 308 | if (categories_total_charge.hasOwnProperty(k)) { |
306 | 309 | categories_total_charge[k] += +d[k] |
307 | 310 | } else { | ... | ... |
app/main/templates/agent.html
1 | 1 | {% extends "base_page.html" %} |
2 | 2 | |
3 | 3 | {% block more_heads %} |
4 | -<link href="{{ url_for('main.static', filename='css/charges.css') }}" rel="stylesheet" type="text/css"/> | |
4 | +<link href="{{ url_for('main.static', filename='css/charges.css', version=config.VERSION) }}" rel="stylesheet" type="text/css"/> | |
5 | 5 | {% endblock %} |
6 | 6 | |
7 | 7 | {% block content %} |
... | ... | @@ -30,11 +30,11 @@ |
30 | 30 | |
31 | 31 | {% block more_scripts %} |
32 | 32 | {% include 'd3js-includes.html' %} |
33 | -<script src="{{ url_for('main.static', filename='js/charges.js') }}" type="text/javascript"></script> | |
33 | +<script src="{{ url_for('main.static', filename='js/charges.js', version=config.VERSION) }}" type="text/javascript"></script> | |
34 | 34 | <script> |
35 | 35 | build_chart("#projects_chart", |
36 | 36 | "{{url_for('main.charge_agent_csv', agent_id=agent.id)}}", |
37 | - "{{agent.name}}", | |
37 | + "{{agent.secondname}}"+ " {{agent.firstname}}", | |
38 | 38 | "project"); |
39 | 39 | </script> |
40 | 40 | {% endblock %} | ... | ... |
app/main/templates/project.html
1 | 1 | {% extends "base_page.html" %} |
2 | 2 | |
3 | 3 | {% block more_heads %} |
4 | -<link href="{{ url_for('main.static', filename='css/charges.css') }}" rel="stylesheet" type="text/css"/> | |
4 | +<link href="{{ url_for('main.static', filename='css/charges.css', version=config.VERSION) }}" rel="stylesheet" type="text/css"/> | |
5 | 5 | {% endblock %} |
6 | 6 | |
7 | 7 | {% block content %} |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | |
32 | 32 | {% block more_scripts %} |
33 | 33 | {% include 'd3js-includes.html' %} |
34 | -<script src="{{ url_for('main.static', filename='js/charges.js') }}" type="text/javascript"></script> | |
34 | +<script src="{{ url_for('main.static', filename='js/charges.js', version=config.VERSION) }}" type="text/javascript"></script> | |
35 | 35 | <script> |
36 | 36 | build_chart("#project_services_chart", |
37 | 37 | "{{url_for('main.charge_project_csv', project_id=project.id, category='service')}}", | ... | ... |
app/templates/base_page.html
... | ... | @@ -2,14 +2,14 @@ |
2 | 2 | <html lang="fr"> |
3 | 3 | <head> |
4 | 4 | {% include 'heads.html' %} |
5 | - <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" type="text/css"/> | |
5 | + <link href="{{ url_for('static', filename='css/style.css', version=config.VERSION) }}" rel="stylesheet" type="text/css"/> | |
6 | 6 | {% block more_heads %} |
7 | 7 | {% endblock %} |
8 | 8 | </head> |
9 | 9 | <body> |
10 | 10 | <nav class="navbar navbar-dark sticky-top bg-dark navbar-expand-lg p-1"> |
11 | 11 | <a class="navbar-brand col-sm-3 col-md-2 " href="{{url_for('main.index')}}"> |
12 | - <img class="m-0 mr-1 {{config.PDC_SITE_CLASS}}" src="{{ url_for('static', filename='img/pdc-ico.png') }}" height="30" width="30"/> | |
12 | + <img class="m-0 mr-1 {{config.PDC_SITE_CLASS}}" src="{{ url_for('static', filename='img/pdc-ico.png', version=config.VERSION) }}" height="30" width="30"/> | |
13 | 13 | {{config.PDC_APP_NAME}} - {{config.PDC_SITE_NAME}}</a> |
14 | 14 | <ul class="navbar-nav ml-auto"> |
15 | 15 | {% if not current_user.is_anonymous %} | ... | ... |
app/templates/heads.html
1 | 1 | <meta charset="utf-8"> |
2 | 2 | <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"> |
3 | - <link href="{{ url_for('static', filename='img/pdc-ico.png') }}" rel="icon"/> | |
3 | + <link href="{{ url_for('static', filename='img/pdc-ico.png', version=config.VERSION) }}" rel="icon"/> | |
4 | 4 | <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" type="text/css"/> |
5 | 5 | <title>{% if title %} {{title}} {% else %}{{config.PDC_APP_NAME}} - {{config.PDC_SITE_NAME}}{%endif%}</title> |
6 | 6 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,28 @@ |
1 | +"""empty message | |
2 | + | |
3 | +Revision ID: c5743e70ac8b | |
4 | +Revises: 4d5e5e207063 | |
5 | +Create Date: 2021-04-15 16:48:32.103879 | |
6 | + | |
7 | +""" | |
8 | +from alembic import op | |
9 | +import sqlalchemy as sa | |
10 | + | |
11 | + | |
12 | +# revision identifiers, used by Alembic. | |
13 | +revision = 'c5743e70ac8b' | |
14 | +down_revision = '4d5e5e207063' | |
15 | +branch_labels = None | |
16 | +depends_on = None | |
17 | + | |
18 | + | |
19 | +def upgrade(): | |
20 | + # ### commands auto generated by Alembic - please adjust! ### | |
21 | + op.add_column('user', sa.Column('password_hash', sa.String(length=128), nullable=True)) | |
22 | + # ### end Alembic commands ### | |
23 | + | |
24 | + | |
25 | +def downgrade(): | |
26 | + # ### commands auto generated by Alembic - please adjust! ### | |
27 | + op.drop_column('user', 'password_hash') | |
28 | + # ### end Alembic commands ### | ... | ... |
resources/flaskenv
... | ... | @@ -4,12 +4,12 @@ |
4 | 4 | # and https://flask.palletsprojects.com/en/1.1.x/config/ |
5 | 5 | # |
6 | 6 | |
7 | -# Default set to 'production', | |
7 | +# Default environment set to 'production', | |
8 | 8 | # but 'development' value adds debuging facities. |
9 | 9 | # |
10 | 10 | FLASK_ENV=development |
11 | 11 | |
12 | -# Is set to the python application wrapper | |
13 | -# you shouldnt edit. | |
12 | +# Default app name is set to the python application wrapper. | |
13 | +# You shouldnt edit unless you know what you're doing. | |
14 | 14 | # |
15 | 15 | FLASK_APP=pdc_web | ... | ... |
resources/lesia-btp.sqlite
No preview for this file type