diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 3f3aee5d3..7d46ddfe4 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: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + formsemestre_id: + metric: + 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: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + formsemestre_id: """ @@ -329,6 +347,16 @@ def assiduites_group(with_query: bool = False): query?est_just=f query?est_just=t + QUERY + ----- + user_id: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + etudids: """ @@ -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: + est_just: + moduleimpl_id: + date_debut: + date_fin: + 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: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + formsemestre_id: + metric: + 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 b3528362a..ad77f54cd 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: + est_just: + date_debut: + date_fin: + etat: + order: + courant: + 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: + est_just: + date_debut: + date_fin: + etat: + order: + courant: + 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: + est_just: + date_debut: + date_fin: + etat: + order: + courant: + 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 5f002d3fe..f1e793c0a 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") + # = 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_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 + 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,25 +177,13 @@ class Token: group.append(child_group) current_y_offset += child.get_height(y_step) + # add `?` circle a:href to element + if self.force_leaf or self.is_leaf(): + group.append(question_mark_group) + 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}" - - path = ET.Element( - "path", - { - "d": path_data, - "style": "stroke:black;stroke-width:2;fill:none", - }, - ) - return path - - def _create_svg_element(text, color="rgb(230,156,190)"): # Dimensions and styling padding = 5 @@ -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: } (ex: {assiduite_id : }) + + La doc doit contenir des lignes de la forme: + + QUERY + ----- + param: + param1: + param2: + + 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": "", + "moduleimpl_id": "", + "count": "", + "formsemestre_id": "", + }, ) - child3 = Token("justificatifs", "POST") + child3 = Token("justificatifs", "POST", func_name="justificatifs_post") root.add_child(child1) child1.add_child(child2)