Commit 5ad3eac3154a28ecf70d8fb46bb1c98aca5323e6
Exists in
master
and in
4 other branches
Charge unit is ETP
Showing
11 changed files
with
74 additions
and
52 deletions
Show diff stats
CHANGELOG.md
... | ... | @@ -24,6 +24,10 @@ or major refactoring improvments. |
24 | 24 | |
25 | 25 | ## Unreleased |
26 | 26 | |
27 | +## [0.3.pre-2] - 2021-04-21 - Display ETP | |
28 | +### Changed | |
29 | +Chart now displays charge in ETP base 1 (was base 100 ) | |
30 | + | |
27 | 31 | ## [0.3.pre-1] - 2021-04-16 - Irap db integration |
28 | 32 | ### New |
29 | 33 | Irap db feed from csv file | ... | ... |
INSTALL.md
... | ... | @@ -3,9 +3,9 @@ |
3 | 3 | ## Prérequis |
4 | 4 | |
5 | 5 | - python3 |
6 | -- sqlite ( pour le développement et les tests unitaires ) | |
7 | -- chrome-driver et chromium ( pour les tests unitaires) | |
8 | -- postgresql ou mysql/mariadb ( pour la production ) | |
6 | +- SQLite (pour le développement et les tests unitaires) | |
7 | +- chrome-driver et chromium (pour les tests unitaires) | |
8 | +- postgresql ou mysql/mariadb (pour la production) | |
9 | 9 | |
10 | 10 | ## Obtenir un répertoire fonctionnel |
11 | 11 | |
... | ... | @@ -23,8 +23,8 @@ |
23 | 23 | |
24 | 24 | ### Configurer l'application |
25 | 25 | |
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. | |
26 | +Les fichiers de configuration fournis dans le répertoire ./resources sont à copier à la racine du projet, mais peuvent | |
27 | +être laissés tels quels pour un premier test après installation. | |
28 | 28 | |
29 | 29 | Il est bon d'y jeter un oeil, les commentaires sont là pour vous aider. |
30 | 30 | (mais en anglais comme tout le code source du projet) |
... | ... | @@ -46,7 +46,7 @@ Il est bon d'y jeter un oeil, les commentaires sont là pour vous aider. |
46 | 46 | cp ./resources/flaskenv ./.flaskenv # ! noter le '.' devant le fichier destination |
47 | 47 | $(EDITOR) .flaskenv |
48 | 48 | |
49 | -### Créer la base de données | |
49 | +### Créer la base de données | |
50 | 50 | |
51 | 51 | Dans un premier temps, pour tester l'installation, on peut s'appuyer sur une base déjà disponible. |
52 | 52 | |
... | ... | @@ -57,12 +57,11 @@ la diffusion de ces données de test avec le projet plan-de-charge. |
57 | 57 | cp resources/lesia-btp.sqlite ./pdc-dev.db |
58 | 58 | |
59 | 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'. | |
60 | +variable `sqlalchemy_devdb_uri`. C'est le cas dans le fichier initial pour une variable `FLASK_ENV` positionnée à ' | |
61 | +development'. | |
62 | 62 | |
63 | 63 | Pour un usage plus avancé, voyez l'outil en ligne de commande fourni avec l'application. |
64 | -(sinon, passez directement à la section suivante ) | |
65 | - | |
64 | +(sinon, passez directement à la section suivante) | |
66 | 65 | |
67 | 66 | # Créer la structure de la base |
68 | 67 | # |
... | ... | @@ -89,7 +88,6 @@ Pour un usage plus avancé, voyez l'outil en ligne de commande fourni avec l'app |
89 | 88 | flask --help |
90 | 89 | flask pdc_db --help |
91 | 90 | |
92 | - | |
93 | 91 | ## Jouer les tests et exécuter un serveur local |
94 | 92 | |
95 | 93 | pip install -r requirements-tests.txt |
... | ... | @@ -100,13 +98,13 @@ Pour un usage plus avancé, voyez l'outil en ligne de commande fourni avec l'app |
100 | 98 | # |
101 | 99 | PYTHONPATH=. pytest --cov=app --cov-report=xml:"coverage.xml" --cov-report=term --junitxml "tests-report.xml" |
102 | 100 | |
103 | -Enfin, ouvrir un serveur sur localhost:5000 et y accéder avec son navigateur. | |
101 | +Enfin, ouvrir un serveur sur `localhost:5000` et y accéder avec son navigateur. | |
104 | 102 | |
105 | 103 | flask run |
106 | 104 | |
107 | -## Configurer l'appli web avec apache | |
105 | +## Configurer l'appli web avec apache | |
108 | 106 | |
109 | -### Les fichiers concernés: | |
107 | +### Les fichiers concernés : | |
110 | 108 | |
111 | 109 | - pdc_web.wsgi |
112 | 110 | - pdc_web.py |
... | ... | @@ -137,14 +135,13 @@ Enfin, ouvrir un serveur sur localhost:5000 et y accéder avec son navigateur. |
137 | 135 | a2ensite pdc-web |
138 | 136 | apachectl restart |
139 | 137 | |
140 | - | |
141 | -## Mise à jour | |
138 | +## Mise à jour | |
142 | 139 | |
143 | 140 | ### git pull |
144 | 141 | |
145 | 142 | ### git autodeploy |
146 | 143 | |
147 | -Les fichiers concernés: | |
144 | +Les fichiers concernés : | |
148 | 145 | |
149 | 146 | - scripts/post-deploy.sh |
150 | 147 | - resources/post-receive.git-hook |
... | ... | @@ -163,8 +160,8 @@ La procédure: |
163 | 160 | |
164 | 161 | ## Gestion des utilisateurs |
165 | 162 | |
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: | |
163 | +La table `users` stocke les utilisateurs qui se connectent à l'applicationa avec leur rôle et les droits associés. Un | |
164 | +ensemble de commandes permet de les gérer : | |
168 | 165 | |
169 | 166 | flask pdc_db user_show_all # liste existante |
170 | 167 | flask pdc_db user_add # ajouter un nouveau login |
... | ... | @@ -172,31 +169,28 @@ Un ensemble de commandes permet de les gérer: |
172 | 169 | flask pdc_db user_delete # effacer un login existant |
173 | 170 | flask pdc_db show_roles # lister les rôles disponibles |
174 | 171 | |
175 | -## Intégration Pycharm | |
172 | +## Intégration Pycharm | |
176 | 173 | |
177 | -Ce projet utilisant le pattern "factory", il faut procéder | |
178 | -à quelques configuration afin de le faire tourner avec | |
174 | +Ce projet utilisant le pattern "factory", il faut procéder à quelques configurations afin de le faire tourner avec | |
179 | 175 | pycharm. |
180 | 176 | |
181 | -Pour une procédure détaillée, voir la page: | |
177 | +Pour une procédure détaillée, voir la page : | |
182 | 178 | |
183 | 179 | https://flask.palletsprojects.com/en/1.1.x/cli/#pycharm-integration |
184 | 180 | |
185 | -Dans le menu 'Edit Configurations', changer les champs: | |
181 | +Dans le menu 'Edit Configurations', changer les champs : | |
186 | 182 | |
187 | 183 | - 'module name' positionné à 'flask' |
188 | -- et dans le champs 'Parameters' choisir 'run' | |
184 | +- et dans le champ 'Parameters' choisir 'run' | |
189 | 185 | |
190 | - | |
191 | -Cette configuration permet de lire les variables positionées dans le fichier .flaskenv | |
186 | +Cette configuration permet de lire les variables positionées dans le fichier `.flaskenv` | |
192 | 187 | |
193 | 188 | FLASK_ENV=development |
194 | 189 | FLASK_APP=pdc_web |
195 | 190 | |
196 | - | |
197 | -Ainsi fait, exécutez votre projet depuis pycharm et essayez sur un navigateur à l'adresse localhost:5000. | |
191 | +Ainsi fait, exécutez votre projet depuis pycharm et essayez sur un navigateur à l'adresse `localhost:5000`. | |
198 | 192 | |
199 | 193 | ## Troubleshooting |
200 | 194 | |
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` | |
195 | +* Q : parfois le module Flask-Migrate n'est pas correctement chargé aprés son installation. | |
196 | +* R : simplement recharger l'environnement virtuel avec `source venv/bin/activate` | ... | ... |
VERSION.txt
app/commands/commands.py
... | ... | @@ -131,6 +131,8 @@ def feed_from_irap(csv_file_name): |
131 | 131 | for period_name in range(2011, 2030): |
132 | 132 | t = Period.query.filter(Period.name == period_name).one() |
133 | 133 | charge = r[f"{period_name}"] |
134 | + # Charge are stored as percent in db, but as fraction of ETP in irap csv | |
135 | + # we make the conversion here. | |
134 | 136 | try: |
135 | 137 | charge = int(100 * float(charge)) |
136 | 138 | except ValueError: | ... | ... |
app/db_mgr.py
1 | -from app.models import db | |
1 | +from app.models import db, Period | |
2 | + | |
3 | +# TODO: make this configurable, and choose another place to use it, | |
4 | +# in 'routes.py' maybe | |
5 | +# Charge as stored on a 100% basis, percent of total worked time; | |
6 | +# Here it is possible to convert to an ETP basis, dividing by 100 | |
7 | +# or we set it to '1' if we want to keep the percent basis | |
8 | +charge_unit = 100 | |
2 | 9 | |
3 | 10 | |
4 | 11 | def projects(): |
... | ... | @@ -8,7 +15,7 @@ def projects(): |
8 | 15 | """ |
9 | 16 | current_period_id = get_current_period() |
10 | 17 | sql_txt = """ |
11 | - select p.id, p.name, sum(tc.charge_rate) as total_charge | |
18 | + select p.id, p.name, IFNULL(sum(tc.charge_rate), 0) as total_charge | |
12 | 19 | from project as p left join |
13 | 20 | ( select c.project_id, c.charge_rate from charge c where c.period_id = {}) |
14 | 21 | tc |
... | ... | @@ -27,7 +34,7 @@ def agents(): |
27 | 34 | """ |
28 | 35 | current_period_id = get_current_period() |
29 | 36 | sql_txt = """ |
30 | - select a.id, a.firstname, a.secondname, sum(tc.charge_rate) as total_charge | |
37 | + select a.id, a.firstname, a.secondname, IFNULL (sum(tc.charge_rate), 0) as total_charge | |
31 | 38 | from agent as a left join |
32 | 39 | ( select c.agent_id, c.charge_rate from charge c where c.period_id = {}) |
33 | 40 | tc |
... | ... | @@ -86,6 +93,8 @@ def charges_by_project_stacked(project_id, category="service"): |
86 | 93 | . |
87 | 94 | . |
88 | 95 | per_n, value_n0, value_n1, ....., value_nn, |
96 | + | |
97 | + TODO: common with charges_by_agent_stacked(agent_id): code to extrat | |
89 | 98 | """ |
90 | 99 | if category == 'capacity': |
91 | 100 | category_table = 'capacity' |
... | ... | @@ -117,7 +126,7 @@ def charges_by_project_stacked(project_id, category="service"): |
117 | 126 | # build the charges line for the current period |
118 | 127 | category_charges = [period_name] |
119 | 128 | for (category_rate,) in db.session.execute(charge_by_categorie_req): |
120 | - category_rate = str(category_rate) if category_rate else '0' | |
129 | + category_rate = str(round(category_rate / charge_unit, 2)) if category_rate else '0' | |
121 | 130 | category_charges.append(category_rate) |
122 | 131 | all_charges.append(category_charges) |
123 | 132 | |
... | ... | @@ -127,6 +136,7 @@ def charges_by_project_stacked(project_id, category="service"): |
127 | 136 | def charges_by_agent_stacked(agent_id): |
128 | 137 | """ |
129 | 138 | Build the list of charges for all projects of one agent, period by period |
139 | + TODO: common with charges_by_project_stacked(project_id, ..) code to extrat | |
130 | 140 | :param agent_id: |
131 | 141 | :return: |
132 | 142 | """ |
... | ... | @@ -143,10 +153,9 @@ def charges_by_agent_stacked(agent_id): |
143 | 153 | group by p.id |
144 | 154 | order by p.id |
145 | 155 | """.format(agent_id, period_id) |
146 | - print(charge_by_project_req) | |
147 | 156 | category_charges = [period_name] |
148 | 157 | for (category_rate,) in db.session.execute(charge_by_project_req): |
149 | - category_rate = str(category_rate) if category_rate else '0' | |
158 | + category_rate = str(round(category_rate / charge_unit, 2)) if category_rate else '0' | |
150 | 159 | category_charges.append(category_rate) |
151 | 160 | all_charges.append(category_charges) |
152 | 161 | return all_charges |
... | ... | @@ -207,4 +216,5 @@ def get_current_period(): |
207 | 216 | :return: the id of the period of current day |
208 | 217 | """ |
209 | 218 | # TODO: request on dates as soon as periods are dated |
210 | - return 14 | |
219 | + p = Period.query.filter((Period.name == '2021') | (Period.name == '2021_S1')).one() | |
220 | + return p.id | ... | ... |
app/main/static/js/charges.js
1 | +function roundToTwo(num) { | |
2 | + return +(Math.round(num + "e+2") + "e-2"); | |
3 | +} | |
4 | + | |
1 | 5 | function build_chart(div_selector, data_url, entity_name, category_type) { |
2 | 6 | |
3 | 7 | const main_elt = document.getElementById("main") |
... | ... | @@ -99,7 +103,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { |
99 | 103 | .duration(200) |
100 | 104 | .style("opacity", 1); |
101 | 105 | tooltip |
102 | - .html("<b>" + category_title + ":</b> " + category_name + "<br>" + "<b>Charge:</b> " + category_charge + "%") | |
106 | + .html("<b>" + category_title + ":</b> " + category_name + "<br>" + "<b>Charge:</b> " + category_charge + " ETP") | |
103 | 107 | .style("left", (e.pageX - tooltip_offset.dx) + "px") |
104 | 108 | .style("top", (e.pageY - tooltip_offset.dy) + "px") |
105 | 109 | } |
... | ... | @@ -120,7 +124,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { |
120 | 124 | .duration(200) |
121 | 125 | .style("opacity", 1) |
122 | 126 | tooltip |
123 | - .html("<b>" + d.period + ": </b>" + d.total + "%") | |
127 | + .html("<b>" + d.period + ": </b>" + d.total + " ETP") | |
124 | 128 | .style("left", (e.pageX - tooltip_offset_dot.dx) + "px") |
125 | 129 | .style("top", (e.pageY - tooltip_offset_dot.dy) + "px") |
126 | 130 | } |
... | ... | @@ -247,7 +251,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { |
247 | 251 | var period_values = Object.values(d).slice(1) |
248 | 252 | var row = {} |
249 | 253 | row['period'] = d.period |
250 | - row['total'] = d3.sum(period_values) | |
254 | + row['total'] = roundToTwo(d3.sum(period_values)) | |
251 | 255 | periods_total_charge.push(row) |
252 | 256 | }); |
253 | 257 | |
... | ... | @@ -308,8 +312,9 @@ function build_chart(div_selector, data_url, entity_name, category_type) { |
308 | 312 | if (categories_total_charge.hasOwnProperty(k)) { |
309 | 313 | categories_total_charge[k] += +d[k] |
310 | 314 | } else { |
311 | - categories_total_charge[k] = d[k] | |
315 | + categories_total_charge[k] = +d[k] | |
312 | 316 | } |
317 | + categories_total_charge[k] = roundToTwo(categories_total_charge[k]) | |
313 | 318 | } |
314 | 319 | ) |
315 | 320 | }) |
... | ... | @@ -384,7 +389,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { |
384 | 389 | .attr("transform", "rotate(-90)") |
385 | 390 | .attr("y", -margin.left + 40) |
386 | 391 | .attr("x", -margin.top - 70) |
387 | - .text("Charge (% ETP)"); | |
392 | + .text("Charge en ETP"); | |
388 | 393 | |
389 | 394 | // |
390 | 395 | // Write chart Title | ... | ... |
app/main/templates/agent.html
... | ... | @@ -17,8 +17,12 @@ |
17 | 17 | <tbody> |
18 | 18 | {% for line in charges[1:] %} |
19 | 19 | <tr> |
20 | - {% for cell in line %} | |
21 | - <td>{{cell}}</td> | |
20 | + {% for i in range(line|length) %} | |
21 | + {%if 'Charge' in charges[0][i] %} | |
22 | + <td>{{line[i]}} %</td> | |
23 | + {%else%} | |
24 | + <td>{{line[i]}}</td> | |
25 | + {%endif%} | |
22 | 26 | {% endfor %} |
23 | 27 | </tr> |
24 | 28 | {% endfor %} | ... | ... |
app/main/templates/agents.html
app/main/templates/projects.html
app/templates/base_page.html
... | ... | @@ -17,11 +17,11 @@ |
17 | 17 | <span class="navbar-brand">{{current_user.name}}</span> |
18 | 18 | </li> |
19 | 19 | <li class="nav-item text-nowrap"> |
20 | - <a class="nav-link" href="{{ url_for('auth.logout') }}">Déconnection</a> | |
20 | + <a class="nav-link" href="{{ url_for('auth.logout') }}">Déconnexion</a> | |
21 | 21 | </li> |
22 | 22 | {% else %} |
23 | 23 | <li class="nav-item text-nowrap"> |
24 | - <a class="nav-link" href="{{ url_for('auth.login') }}">Connection</a> | |
24 | + <a class="nav-link" href="{{ url_for('auth.login') }}">Connexion</a> | |
25 | 25 | </li> |
26 | 26 | {% endif %} |
27 | 27 | </ul> | ... | ... |
resources/pdc_config.py
... | ... | @@ -96,6 +96,11 @@ class DevConfig(Config): |
96 | 96 | # ignores @role_required decorator |
97 | 97 | ROLE_DISABLED = True |
98 | 98 | |
99 | + from datetime import datetime | |
100 | + import re | |
101 | + date = datetime.now().strftime("%y%m%d%H%M") | |
102 | + VERSION = re.sub(r"^(\d\.\d)\..*$", r"\1" + f".pre-{date}", Config.VERSION) | |
103 | + | |
99 | 104 | |
100 | 105 | # Testing configuration |
101 | 106 | # | ... | ... |