Commit 5ad3eac3154a28ecf70d8fb46bb1c98aca5323e6

Authored by hitier
2 parents 96ef779b 2ae54c01

Charge unit is ETP

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
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 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
... ... @@ -15,7 +15,7 @@
15 15 {{ agent.firstname }}
16 16 {{ agent.secondname }}
17 17 </a></td>
18   - <td>{{agent.total_charge}}</td>
  18 + <td>{{agent.total_charge}} %</td>
19 19 <td>{{agent.num_projects}}</td>
20 20 </tr>
21 21 {% endfor %}
... ...
app/main/templates/projects.html
... ... @@ -13,9 +13,7 @@
13 13 <td><a href="{{url_for('main.project', project_id=project.id)}}">
14 14 {{ project.name }}</a>
15 15 </td>
16   - <td>
17   - {{ project.total_charge }}</a>
18   - </td>
  16 + <td>{{ project.total_charge }} % </td>
19 17 </tr>
20 18 {% endfor %}
21 19 </tbody>
... ...
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 #
... ...