Commit 96ef779b1109faa864be5c47ddc25b66eaed7bd6

Authored by hitier
2 parents c25197fc f8f2c15b

Irap db Integration

Feed database with charges from irap csv file
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
1   -0.2.1
  1 +0.3.pre-1
... ...
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
... ...
migrations/versions/c5743e70ac8b_.py 0 → 100644
... ... @@ -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