Coverage for app / services / category_service.py: 100%
45 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-06 04:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-06 04:49 +0000
1"""
2This module provides tools for managing book categories and their hierarchy.
4It includes functionality to encode and decode category IDs, construct category
5trees, and fetch unique categories from the database. The module is designed for
6building Bootstrap-compatible tree structures for UI representations.
7"""
8import base64
10from app import db
11from app.models import Book
14def get_category_bs_tree():
15 """
16 Constructs and returns a Bootstrap-formatted tree representation of categories.
18 The function retrieves a hierarchical category tree and processes it into a
19 format suitable for a Bootstrap UI component. Each item in the resulting tree
20 contains metadata such as the category name, a unique id based on its full path,
21 and a checked state.
23 :raises ValueError: If the category tree data is malformed.
25 :return: A list of dictionaries representing the category tree in a structure
26 compatible with Bootstrap frameworks.
27 :rtype: list[dict]
28 """
29 def _add_categories(cat, context, tree, children):
30 """
31 Adds a category and its subcategories to the tree representation.
33 The function recursively creates a tree structure, where each node represents
34 a category. Each category node is stored as a dictionary with information about
35 the category's text, state (e.g., checked or unchecked), the full path to the
36 category, and an identifier. If the category has children, they are also added
37 to the tree recursively.
39 :param cat: The current category name being added.
40 :type cat: str
41 :param context: The current hierarchical path leading to the category.
42 :type context: str
43 :param tree: A list representing the current level of the tree structure.
44 :type tree: list
45 :param children: A dictionary of child categories, where the keys are child
46 category names, and the values are their respective sub-children.
47 :type children: dict
48 :return: None
49 """
50 fullpath = context + _SEPARATOR + cat if context else cat
51 node = {
52 "text": cat,
53 "state": {"checked": False},
54 "fullpath": fullpath,
55 "id": _fullpath_to_id(fullpath)
56 }
57 tree.append(node)
58 if children:
59 node["nodes"] = []
60 for child in children:
61 _add_categories(child, fullpath, node["nodes"], children[child])
63 categories = _get_category_tree()
64 bs_tree = []
65 for category, subcategories in categories.items():
66 _add_categories(category, '', bs_tree, subcategories)
67 return bs_tree
70def id_to_fullpath(encoded_id):
71 """
72 Decodes a URL-safe HTML id string back into the original fullpath using Base64.
74 The function reverses the transformations applied in `fullpath_to_id`, ensuring
75 that the output matches the original input string.
77 :param encoded_id: The Base64-encoded URL-safe HTML id string to be decoded.
78 :type encoded_id: str
79 :return: The original fullpath string.
80 :rtype: str
81 """
82 safe_decoded = (encoded_id
83 .replace('-', '+')
84 .replace('_', '/')
85 .replace('*', '='))
86 decoded = base64.b64decode(safe_decoded).decode('utf-8')
87 return decoded
90def _get_category_list():
91 """
92 Fetches a list of unique book categories from the database sorted in alphabetical
93 order.
95 The function establishes a database connection and executes an SQL query to
96 retrieve distinct book categories from the database. After
97 retrieving the results, the function processes and returns the list of unique
98 categories.
100 :raises sqlalchemy.exc.SQLAlchemyError: If there is an error executing the
101 SQL query or an issue with the database connection.
103 :return: A list of strings containing distinct book categories sorted
104 alphabetically.
105 :rtype: list[str]
106 """
107 result = (db.session.query(Book.categories_flat)
108 .distinct()
109 .order_by(Book.categories_flat)
110 .all())
112 # Extracting the results into a list
113 categories = [row.categories_flat for row in result]
114 return categories
117_SEPARATOR = ' > ' # separator for full category strings
120def _get_category_tree():
121 """
122 Builds a hierarchical tree of categories from a flat list of category strings
123 separated by ' > '. Each category string represents a path, where top-level
124 categories are separated by '>' from their respective subcategories.
126 The function `get_category_tree` iterates through a list of categories to
127 construct a nested dictionary structure representing the hierarchical
128 relationship among categories. An auxiliary recursive function `_add_categories`
129 is used to process each category path and build the tree.
131 :raises None: This function does not raise any errors.
133 :return: A dictionary where keys are category names and values are nested
134 dictionaries representing subcategories.
135 :rtype: dict
136 """
137 def _add_categories(cat, tree, sub_categories):
138 if cat not in tree:
139 tree[cat] = {}
140 if sub_categories:
141 # remove first subcategory and make sub_categories list smaller
142 sub_category = sub_categories.pop(0)
143 _add_categories(sub_category, tree[cat], sub_categories)
145 categories = _get_category_list()
146 category_tree = {}
147 for category in categories:
148 categories = category.split(_SEPARATOR)
149 top_level_category = categories.pop(0)
150 _add_categories(top_level_category, category_tree, categories)
151 return category_tree
154def _fullpath_to_id(fullpath):
155 """
156 Converts a fullpath string into a URL-safe HTML id by encoding it using Base64.
158 The transformation encodes the input as a Base64 string, replaces URL-sensitive
159 characters, and ensures that the resulting id strings are unique and reversible.
161 :param fullpath: The original fullpath string to be converted.
162 :type fullpath: str
163 :return: A URL-safe Base64-encoded HTML id string.
164 :rtype: str
165 """
166 encoded = base64.b64encode(fullpath.encode('utf-8')).decode('utf-8')
167 safe_encoded = (encoded
168 .replace('+', '-')
169 .replace('/', '_')
170 .replace('=', '*'))
171 return safe_encoded
174__all__ = ['get_category_bs_tree', 'id_to_fullpath']