diff --git a/app/api/assiduites.py b/app/api/assiduites.py
index 3f3aee5d3da5af566d1f6a88d4a67809b32e5b63..7d46ddfe4be2912a67ee13dfaabb7a29933cb3bd 100644
--- a/app/api/assiduites.py
+++ b/app/api/assiduites.py
@@ -161,8 +161,17 @@ def count_assiduites(
query?est_just=f
query?est_just=t
-
-
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ moduleimpl_id:<int:moduleimpl_id>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ formsemestre_id:<int:formsemestre_id>
+ metric:<array[string]:metric>
+ split:<bool:split>
"""
@@ -253,6 +262,15 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
query?est_just=f
query?est_just=t
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ moduleimpl_id:<int:moduleimpl_id>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ formsemestre_id:<int:formsemestre_id>
"""
@@ -329,6 +347,16 @@ def assiduites_group(with_query: bool = False):
query?est_just=f
query?est_just=t
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ moduleimpl_id:<int:moduleimpl_id>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ etudids:<array[int]:etudids
+ formsemestre_id:<int:formsemestre_id>
"""
@@ -388,7 +416,16 @@ def assiduites_group(with_query: bool = False):
@as_json
@permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
- """Retourne toutes les assiduités du formsemestre"""
+ """Retourne toutes les assiduités du formsemestre
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ moduleimpl_id:<int:moduleimpl_id>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ """
# Récupération du formsemestre à partir du formsemestre_id
formsemestre: FormSemestre = None
@@ -438,7 +475,20 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
def count_assiduites_formsemestre(
formsemestre_id: int = None, with_query: bool = False
):
- """Comptage des assiduités du formsemestre"""
+ """Comptage des assiduités du formsemestre
+
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ moduleimpl_id:<int:moduleimpl_id>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ formsemestre_id:<int:formsemestre_id>
+ metric:<array[string]:metric>
+ split:<bool:split>
+ """
# Récupération du formsemestre à partir du formsemestre_id
formsemestre: FormSemestre = None
diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py
index b3528362a0df78cbe3d8553c20fef72b5c639ef4..ad77f54cda27258bdbbf3862b7e273c1503c1773 100644
--- a/app/api/justificatifs.py
+++ b/app/api/justificatifs.py
@@ -3,8 +3,8 @@
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
-"""ScoDoc 9 API : Justificatifs
-"""
+"""ScoDoc 9 API : Justificatifs"""
+
from datetime import datetime
from flask_json import as_json
@@ -113,6 +113,16 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
user_id (l'id de l'auteur du justificatif)
query?user_id=[int]
ex query?user_id=3
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ order:<bool:order>
+ courant:<bool:courant>
+ group_id:<int:group_id>
"""
# Récupération de l'étudiant
etud: Identite = tools.get_etud(etudid, nip, ine)
@@ -154,6 +164,17 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
"""
Renvoie tous les justificatifs d'un département
(en ajoutant un champ "formsemestre" si possible)
+
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ order:<bool:order>
+ courant:<bool:courant>
+ group_id:<int:group_id>
"""
# Récupération du département et des étudiants du département
@@ -225,7 +246,19 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
@as_json
@permission_required(Permission.ScoView)
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
- """Retourne tous les justificatifs du formsemestre"""
+ """Retourne tous les justificatifs du formsemestre
+
+ QUERY
+ -----
+ user_id:<int:user_id>
+ est_just:<bool:est_just>
+ date_debut:<string:date_debut_iso>
+ date_fin:<string:date_fin_iso>
+ etat:<array[string]:etat>
+ order:<bool:order>
+ courant:<bool:courant>
+ group_id:<int:group_id>
+ """
# Récupération du formsemestre
formsemestre: FormSemestre = None
diff --git a/tools/create_api_map.py b/tools/create_api_map.py
index 5f002d3fe68d22422d26dac1a4e4284c8da1003f..f1e793c0ae87763871dd091a5332eef61058d246 100644
--- a/tools/create_api_map.py
+++ b/tools/create_api_map.py
@@ -1,4 +1,5 @@
import xml.etree.ElementTree as ET
+import re
class COLORS:
@@ -6,16 +7,16 @@ class COLORS:
GREEN = "rgb(165,214,165)"
PINK = "rgb(230,156,190)"
GREY = "rgb(224,224,224)"
- ORANGE = "rgb(253,191,111)"
class Token:
- def __init__(self, name, method="GET", query=None, leaf=False):
+ def __init__(self, name, method="GET", query=None, leaf=False, func_name=""):
self.children: list["Token"] = []
self.name: str = name
self.method: str = method
self.query: dict[str, str] = query or {}
self.force_leaf: bool = leaf
+ self.func_name = ""
def add_child(self, child):
self.children.append(child)
@@ -56,17 +57,23 @@ class Token:
):
group = ET.Element("g")
color = COLORS.BLUE
- if self.force_leaf or self.is_leaf():
+ if self.is_leaf():
if self.method == "GET":
color = COLORS.GREEN
elif self.method == "POST":
color = COLORS.PINK
+ # if self.force_leaf and not self.is_leaf():
+ # color = COLORS.ORANGE
element = _create_svg_element(self.name, color)
element.set("transform", f"translate({x_offset}, {y_offset})")
current_start_coords, current_end_coords = _get_anchor_coords(
element, x_offset, y_offset
)
+ href = "#" + self.func_name.replace("_", "-")
+ if self.query:
+ href += "-query"
+ question_mark_group = _create_question_mark_group(current_end_coords, href)
group.append(element)
# Add an arrow from the parent element to the current element
@@ -91,9 +98,10 @@ class Token:
current_end_coords = _get_anchor_coords(group, 0, 0)[1]
query_y_offset = y_offset
- ampersand_start_coords = None
- ampersand_end_coords = None
+ query_sub_element = ET.Element("g")
for key, value in self.query.items():
+ sub_group = ET.Element("g")
+
# <param>=<value>
translate_x = x_offset + x_step
@@ -103,14 +111,14 @@ class Token:
"transform",
f"translate({translate_x}, {query_y_offset})",
)
- group.append(param_el)
+ sub_group.append(param_el)
# add Arrow from "query" to element
coords = (
current_end_coords,
_get_anchor_coords(param_el, translate_x, query_y_offset)[0],
)
- group.append(_create_arrow(*coords))
+ sub_group.append(_create_arrow(*coords))
# =
equal_el = _create_svg_element("=", COLORS.GREY)
@@ -119,7 +127,7 @@ class Token:
"transform",
f"translate({translate_x}, {query_y_offset})",
)
- group.append(equal_el)
+ sub_group.append(equal_el)
# <value>
value_el = _create_svg_element(value, COLORS.GREEN)
@@ -132,7 +140,7 @@ class Token:
"transform",
f"translate({translate_x}, {query_y_offset})",
)
- group.append(value_el)
+ sub_group.append(value_el)
if len(self.query) == 1:
continue
ampersand_group = _create_svg_element("&", "rgb(224,224,224)")
@@ -145,27 +153,15 @@ class Token:
"transform",
f"translate({translate_x}, {query_y_offset})",
)
- group.append(ampersand_group)
-
- # Track the start and end coordinates of the ampersands
- if ampersand_start_coords is None:
- ampersand_start_coords = _get_anchor_coords(
- ampersand_group, translate_x, query_y_offset
- )[1]
- ampersand_end_coords = _get_anchor_coords(
- ampersand_group, translate_x, query_y_offset
- )[1]
- # Draw line connecting all ampersands
- if ampersand_start_coords and ampersand_end_coords and len(self.query) > 1:
- line = _create_line(ampersand_start_coords, ampersand_end_coords)
- group.append(line)
+ sub_group.append(ampersand_group)
query_y_offset += y_step
- y_offset = query_y_offset
+ query_sub_element.append(sub_group)
+ group.append(query_sub_element)
+ y_offset = query_y_offset
current_y_offset = y_offset
-
for child in self.children:
rel_x_offset = x_offset + _get_group_width(group)
if len(self.children) > 1:
@@ -181,23 +177,11 @@ class Token:
group.append(child_group)
current_y_offset += child.get_height(y_step)
- return group
-
-
-def _create_line(start_coords, end_coords):
- start_x, start_y = start_coords
- end_x, end_y = end_coords
-
- path_data = f"M {start_x},{start_y} L {start_x + 20},{start_y} L {start_x + 20},{end_y} L {end_x},{end_y}"
+ # add `?` circle a:href to element
+ if self.force_leaf or self.is_leaf():
+ group.append(question_mark_group)
- path = ET.Element(
- "path",
- {
- "d": path_data,
- "style": "stroke:black;stroke-width:2;fill:none",
- },
- )
- return path
+ return group
def _create_svg_element(text, color="rgb(230,156,190)"):
@@ -289,6 +273,51 @@ def _get_group_width(group):
return sum(_get_element_width(child) for child in group)
+def _create_question_mark_group(coords, href):
+ x, y = coords
+ radius = 10 # Radius of the circle
+ y -= radius * 2
+ font_size = 17 # Font size of the question mark
+
+ group = ET.Element("g")
+
+ # Create the circle
+ ET.SubElement(
+ group,
+ "circle",
+ {
+ "cx": str(x),
+ "cy": str(y),
+ "r": str(radius),
+ "fill": COLORS.GREY,
+ "stroke": "black",
+ "stroke-width": "2",
+ },
+ )
+
+ # Create the link element
+ link = ET.Element("a", {"href": href})
+
+ # Create the text element
+ text_element = ET.SubElement(
+ link,
+ "text",
+ {
+ "x": str(x + 1),
+ "y": str(y + font_size / 3), # Adjust to vertically center the text
+ "text-anchor": "middle", # Center the text horizontally
+ "font-family": "Arial",
+ "font-size": str(font_size),
+ "fill": "black",
+ },
+ )
+ text_element.text = "?"
+
+ group.append(link)
+
+ return group
+
+
def gen_api_map(app):
api_map = Token("")
for rule in app.url_map.iter_rules():
@@ -303,15 +332,19 @@ def gen_api_map(app):
# Check if the segment already exists in the current level
child = current_token.find_child(segment)
if child is None:
- # If it's the last segment and it has query parameters
- if i == len(segments) - 1 and segment == "query":
+ func = app.view_functions[rule.endpoint]
+ # If it's the last segment
+ if i == len(segments) - 1:
child = Token(
segment,
leaf=True,
+ query=parse_query_doc(func.__doc__ or ""),
)
- # TODO Parse QUERY doc
else:
- child = Token(segment)
+ child = Token(
+ segment,
+ )
+ child.func_name = func.__name__
method: str = "POST" if "POST" in rule.methods else "GET"
child.method = method
current_token.add_child(child)
@@ -322,6 +355,10 @@ def gen_api_map(app):
current_token.force_leaf = True
generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg")
+ print(
+ "La carte a été générée avec succès. "
+ + "Vous pouvez la consulter à l'adresse suivante : /tmp/api_map.svg"
+ )
def _get_bbox(element, x_offset=0, y_offset=0):
@@ -366,8 +403,8 @@ def _get_bbox(element, x_offset=0, y_offset=0):
def generate_svg(element, fname):
bbox = _get_bbox(element)
- width = bbox["x_max"] - bbox["x_min"] + 20 # Add some padding
- height = bbox["y_max"] - bbox["y_min"] + 20 # Add some padding
+ width = bbox["x_max"] - bbox["x_min"] + 80 # Add some padding
+ height = bbox["y_max"] - bbox["y_min"] + 80 # Add some padding
svg = ET.Element(
"svg",
@@ -401,15 +438,59 @@ def generate_svg(element, fname):
tree.write(fname, encoding="utf-8", xml_declaration=True)
+def parse_query_doc(doc):
+ """
+ renvoie un dictionnaire {param: <type:nom_param>} (ex: {assiduite_id : <int:assiduite_id>})
+
+ La doc doit contenir des lignes de la forme:
+
+ QUERY
+ -----
+ param:<string:nom_param>
+ param1:<int:num>
+ param2:<array[string]:array_nom>
+
+ Dès qu'une ligne ne respecte pas ce format (voir regex dans la fonction), on arrête de parser
+ Attention, la ligne ----- doit être collée contre QUERY et contre le premier paramètre
+ """
+
+ lines = [line.strip() for line in doc.split("\n")]
+ try:
+ query_index = lines.index("QUERY")
+ if lines[query_index + 1] != "-----":
+ return {}
+ except ValueError:
+ return {}
+
+ query_lines = lines[query_index + 2 :]
+
+ query = {}
+ regex = re.compile(r"^(\w+):(<.+>)$")
+ for line in query_lines:
+ parts = regex.match(line)
+ if not parts:
+ break
+ param, type_nom_param = parts.groups()
+ query[param] = type_nom_param
+
+ return query
+
+
if __name__ == "__main__":
root = Token("api")
- child1 = Token("assiduites", leaf=True)
+ child1 = Token("assiduites", leaf=True, func_name="assiduites_get")
child2 = Token("count")
child22 = Token("all")
child23 = Token(
- "query", query={"param1": "value1", "param2": "value2", "param3": "value3"}
+ "query",
+ query={
+ "etat": "<string:etat>",
+ "moduleimpl_id": "<int:moduleimpl_id>",
+ "count": "<int:count>",
+ "formsemestre_id": "<int:formsemestre_id>",
+ },
)
- child3 = Token("justificatifs", "POST")
+ child3 = Token("justificatifs", "POST", func_name="justificatifs_post")
root.add_child(child1)
child1.add_child(child2)