From 06727f1b9b2ceb2dc28b892178db9228ab224b42 Mon Sep 17 00:00:00 2001
From: Iziram <matthias.hartmann@iziram.fr>
Date: Thu, 20 Jun 2024 17:56:12 +0200
Subject: [PATCH] gen_api_map fini + annotation QUERY assiduites/justificatifs

---
 app/api/assiduites.py    |  58 ++++++++++++-
 app/api/justificatifs.py |  39 ++++++++-
 tools/create_api_map.py  | 181 ++++++++++++++++++++++++++++-----------
 3 files changed, 221 insertions(+), 57 deletions(-)

diff --git a/app/api/assiduites.py b/app/api/assiduites.py
index 3f3aee5d..7d46ddfe 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 b3528362..ad77f54c 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 5f002d3f..f1e793c0 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)
-- 
GitLab