Commit 5ad3eac3154a28ecf70d8fb46bb1c98aca5323e6

Authored by hitier
2 parents 96ef779b 2ae54c01

Charge unit is ETP

@@ -24,6 +24,10 @@ or major refactoring improvments. @@ -24,6 +24,10 @@ or major refactoring improvments.
24 24
25 ## Unreleased 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 ## [0.3.pre-1] - 2021-04-16 - Irap db integration 31 ## [0.3.pre-1] - 2021-04-16 - Irap db integration
28 ### New 32 ### New
29 Irap db feed from csv file 33 Irap db feed from csv file
@@ -3,9 +3,9 @@ @@ -3,9 +3,9 @@
3 ## Prérequis 3 ## Prérequis
4 4
5 - python3 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 ## Obtenir un répertoire fonctionnel 10 ## Obtenir un répertoire fonctionnel
11 11
@@ -23,8 +23,8 @@ @@ -23,8 +23,8 @@
23 23
24 ### Configurer l'application 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 Il est bon d'y jeter un oeil, les commentaires sont là pour vous 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 (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,7 +46,7 @@ Il est bon d'y jeter un oeil, les commentaires sont là pour vous aider.
46 cp ./resources/flaskenv ./.flaskenv # ! noter le '.' devant le fichier destination 46 cp ./resources/flaskenv ./.flaskenv # ! noter le '.' devant le fichier destination
47 $(EDITOR) .flaskenv 47 $(EDITOR) .flaskenv
48 48
49 -### Créer la base de données 49 +### Créer la base de données
50 50
51 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.
52 52
@@ -57,12 +57,11 @@ la diffusion de ces données de test avec le projet plan-de-charge. @@ -57,12 +57,11 @@ la diffusion de ces données de test avec le projet plan-de-charge.
57 cp resources/lesia-btp.sqlite ./pdc-dev.db 57 cp resources/lesia-btp.sqlite ./pdc-dev.db
58 58
59 Vérifier que ce chemin correspond avec celui configuré dans le fichier `db_config.py` pour la 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 Pour un usage plus avancé, voyez l'outil en ligne de commande fourni avec l'application. 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 # Créer la structure de la base 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,7 +88,6 @@ Pour un usage plus avancé, voyez l'outil en ligne de commande fourni avec l'app
89 flask --help 88 flask --help
90 flask pdc_db --help 89 flask pdc_db --help
91 90
92 -  
93 ## Jouer les tests et exécuter un serveur local 91 ## Jouer les tests et exécuter un serveur local
94 92
95 pip install -r requirements-tests.txt 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,13 +98,13 @@ Pour un usage plus avancé, voyez l'outil en ligne de commande fourni avec l'app
100 # 98 #
101 PYTHONPATH=. pytest --cov=app --cov-report=xml:"coverage.xml" --cov-report=term --junitxml "tests-report.xml" 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 flask run 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 - pdc_web.wsgi 109 - pdc_web.wsgi
112 - pdc_web.py 110 - pdc_web.py
@@ -137,14 +135,13 @@ Enfin, ouvrir un serveur sur localhost:5000 et y accéder avec son navigateur. @@ -137,14 +135,13 @@ Enfin, ouvrir un serveur sur localhost:5000 et y accéder avec son navigateur.
137 a2ensite pdc-web 135 a2ensite pdc-web
138 apachectl restart 136 apachectl restart
139 137
140 -  
141 -## Mise à jour 138 +## Mise à jour
142 139
143 ### git pull 140 ### git pull
144 141
145 ### git autodeploy 142 ### git autodeploy
146 143
147 -Les fichiers concernés: 144 +Les fichiers concernés :
148 145
149 - scripts/post-deploy.sh 146 - scripts/post-deploy.sh
150 - resources/post-receive.git-hook 147 - resources/post-receive.git-hook
@@ -163,8 +160,8 @@ La procédure: @@ -163,8 +160,8 @@ La procédure:
163 160
164 ## Gestion des utilisateurs 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 flask pdc_db user_show_all # liste existante 166 flask pdc_db user_show_all # liste existante
170 flask pdc_db user_add # ajouter un nouveau login 167 flask pdc_db user_add # ajouter un nouveau login
@@ -172,31 +169,28 @@ Un ensemble de commandes permet de les gérer: @@ -172,31 +169,28 @@ Un ensemble de commandes permet de les gérer:
172 flask pdc_db user_delete # effacer un login existant 169 flask pdc_db user_delete # effacer un login existant
173 flask pdc_db show_roles # lister les rôles disponibles 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 pycharm. 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 https://flask.palletsprojects.com/en/1.1.x/cli/#pycharm-integration 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 - 'module name' positionné à 'flask' 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 FLASK_ENV=development 188 FLASK_ENV=development
194 FLASK_APP=pdc_web 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 ## Troubleshooting 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`
1 -0.3.pre-1 1 +0.3.pre-2
app/commands/commands.py
@@ -131,6 +131,8 @@ def feed_from_irap(csv_file_name): @@ -131,6 +131,8 @@ def feed_from_irap(csv_file_name):
131 for period_name in range(2011, 2030): 131 for period_name in range(2011, 2030):
132 t = Period.query.filter(Period.name == period_name).one() 132 t = Period.query.filter(Period.name == period_name).one()
133 charge = r[f"{period_name}"] 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 try: 136 try:
135 charge = int(100 * float(charge)) 137 charge = int(100 * float(charge))
136 except ValueError: 138 except ValueError:
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 def projects(): 11 def projects():
@@ -8,7 +15,7 @@ def projects(): @@ -8,7 +15,7 @@ def projects():
8 """ 15 """
9 current_period_id = get_current_period() 16 current_period_id = get_current_period()
10 sql_txt = """ 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 from project as p left join 19 from project as p left join
13 ( select c.project_id, c.charge_rate from charge c where c.period_id = {}) 20 ( select c.project_id, c.charge_rate from charge c where c.period_id = {})
14 tc 21 tc
@@ -27,7 +34,7 @@ def agents(): @@ -27,7 +34,7 @@ def agents():
27 """ 34 """
28 current_period_id = get_current_period() 35 current_period_id = get_current_period()
29 sql_txt = """ 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 from agent as a left join 38 from agent as a left join
32 ( select c.agent_id, c.charge_rate from charge c where c.period_id = {}) 39 ( select c.agent_id, c.charge_rate from charge c where c.period_id = {})
33 tc 40 tc
@@ -86,6 +93,8 @@ def charges_by_project_stacked(project_id, category="service"): @@ -86,6 +93,8 @@ def charges_by_project_stacked(project_id, category="service"):
86 . 93 .
87 . 94 .
88 per_n, value_n0, value_n1, ....., value_nn, 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 if category == 'capacity': 99 if category == 'capacity':
91 category_table = 'capacity' 100 category_table = 'capacity'
@@ -117,7 +126,7 @@ def charges_by_project_stacked(project_id, category="service"): @@ -117,7 +126,7 @@ def charges_by_project_stacked(project_id, category="service"):
117 # build the charges line for the current period 126 # build the charges line for the current period
118 category_charges = [period_name] 127 category_charges = [period_name]
119 for (category_rate,) in db.session.execute(charge_by_categorie_req): 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 category_charges.append(category_rate) 130 category_charges.append(category_rate)
122 all_charges.append(category_charges) 131 all_charges.append(category_charges)
123 132
@@ -127,6 +136,7 @@ def charges_by_project_stacked(project_id, category="service"): @@ -127,6 +136,7 @@ def charges_by_project_stacked(project_id, category="service"):
127 def charges_by_agent_stacked(agent_id): 136 def charges_by_agent_stacked(agent_id):
128 """ 137 """
129 Build the list of charges for all projects of one agent, period by period 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 :param agent_id: 140 :param agent_id:
131 :return: 141 :return:
132 """ 142 """
@@ -143,10 +153,9 @@ def charges_by_agent_stacked(agent_id): @@ -143,10 +153,9 @@ def charges_by_agent_stacked(agent_id):
143 group by p.id 153 group by p.id
144 order by p.id 154 order by p.id
145 """.format(agent_id, period_id) 155 """.format(agent_id, period_id)
146 - print(charge_by_project_req)  
147 category_charges = [period_name] 156 category_charges = [period_name]
148 for (category_rate,) in db.session.execute(charge_by_project_req): 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 category_charges.append(category_rate) 159 category_charges.append(category_rate)
151 all_charges.append(category_charges) 160 all_charges.append(category_charges)
152 return all_charges 161 return all_charges
@@ -207,4 +216,5 @@ def get_current_period(): @@ -207,4 +216,5 @@ def get_current_period():
207 :return: the id of the period of current day 216 :return: the id of the period of current day
208 """ 217 """
209 # TODO: request on dates as soon as periods are dated 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 function build_chart(div_selector, data_url, entity_name, category_type) { 5 function build_chart(div_selector, data_url, entity_name, category_type) {
2 6
3 const main_elt = document.getElementById("main") 7 const main_elt = document.getElementById("main")
@@ -99,7 +103,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) { @@ -99,7 +103,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
99 .duration(200) 103 .duration(200)
100 .style("opacity", 1); 104 .style("opacity", 1);
101 tooltip 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 .style("left", (e.pageX - tooltip_offset.dx) + "px") 107 .style("left", (e.pageX - tooltip_offset.dx) + "px")
104 .style("top", (e.pageY - tooltip_offset.dy) + "px") 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,7 +124,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
120 .duration(200) 124 .duration(200)
121 .style("opacity", 1) 125 .style("opacity", 1)
122 tooltip 126 tooltip
123 - .html("<b>" + d.period + ": </b>" + d.total + "%") 127 + .html("<b>" + d.period + ": </b>" + d.total + " ETP")
124 .style("left", (e.pageX - tooltip_offset_dot.dx) + "px") 128 .style("left", (e.pageX - tooltip_offset_dot.dx) + "px")
125 .style("top", (e.pageY - tooltip_offset_dot.dy) + "px") 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,7 +251,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
247 var period_values = Object.values(d).slice(1) 251 var period_values = Object.values(d).slice(1)
248 var row = {} 252 var row = {}
249 row['period'] = d.period 253 row['period'] = d.period
250 - row['total'] = d3.sum(period_values) 254 + row['total'] = roundToTwo(d3.sum(period_values))
251 periods_total_charge.push(row) 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,8 +312,9 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
308 if (categories_total_charge.hasOwnProperty(k)) { 312 if (categories_total_charge.hasOwnProperty(k)) {
309 categories_total_charge[k] += +d[k] 313 categories_total_charge[k] += +d[k]
310 } else { 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,7 +389,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
384 .attr("transform", "rotate(-90)") 389 .attr("transform", "rotate(-90)")
385 .attr("y", -margin.left + 40) 390 .attr("y", -margin.left + 40)
386 .attr("x", -margin.top - 70) 391 .attr("x", -margin.top - 70)
387 - .text("Charge (% ETP)"); 392 + .text("Charge en ETP");
388 393
389 // 394 //
390 // Write chart Title 395 // Write chart Title
app/main/templates/agent.html
@@ -17,8 +17,12 @@ @@ -17,8 +17,12 @@
17 <tbody> 17 <tbody>
18 {% for line in charges[1:] %} 18 {% for line in charges[1:] %}
19 <tr> 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 {% endfor %} 26 {% endfor %}
23 </tr> 27 </tr>
24 {% endfor %} 28 {% endfor %}
app/main/templates/agents.html
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 {{ agent.firstname }} 15 {{ agent.firstname }}
16 {{ agent.secondname }} 16 {{ agent.secondname }}
17 </a></td> 17 </a></td>
18 - <td>{{agent.total_charge}}</td> 18 + <td>{{agent.total_charge}} %</td>
19 <td>{{agent.num_projects}}</td> 19 <td>{{agent.num_projects}}</td>
20 </tr> 20 </tr>
21 {% endfor %} 21 {% endfor %}
app/main/templates/projects.html
@@ -13,9 +13,7 @@ @@ -13,9 +13,7 @@
13 <td><a href="{{url_for('main.project', project_id=project.id)}}"> 13 <td><a href="{{url_for('main.project', project_id=project.id)}}">
14 {{ project.name }}</a> 14 {{ project.name }}</a>
15 </td> 15 </td>
16 - <td>  
17 - {{ project.total_charge }}</a>  
18 - </td> 16 + <td>{{ project.total_charge }} % </td>
19 </tr> 17 </tr>
20 {% endfor %} 18 {% endfor %}
21 </tbody> 19 </tbody>
app/templates/base_page.html
@@ -17,11 +17,11 @@ @@ -17,11 +17,11 @@
17 <span class="navbar-brand">{{current_user.name}}</span> 17 <span class="navbar-brand">{{current_user.name}}</span>
18 </li> 18 </li>
19 <li class="nav-item text-nowrap"> 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 </li> 21 </li>
22 {% else %} 22 {% else %}
23 <li class="nav-item text-nowrap"> 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 </li> 25 </li>
26 {% endif %} 26 {% endif %}
27 </ul> 27 </ul>
resources/pdc_config.py
@@ -96,6 +96,11 @@ class DevConfig(Config): @@ -96,6 +96,11 @@ class DevConfig(Config):
96 # ignores @role_required decorator 96 # ignores @role_required decorator
97 ROLE_DISABLED = True 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 # Testing configuration 105 # Testing configuration
101 # 106 #