Coverage for app / services / tag_service.py: 21%
46 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"""
2 Database service routines associated with tags.
3"""
4import re
6import bleach
8from app import db
9from app.helpers.tag_colors import choose_random_color
10from app.models import Tag, TagBook
13def get_tags_for_user(user_id, q='') -> list[Tag]:
14 """
15 Retrieve a list of tags associated with a specific user. Optionally, filter the
16 tags by a search query string.
18 :param user_id: The unique identifier of the user whose tags are to be retrieved.
19 :type user_id: int
20 :param q: An optional search query string to filter the tags by their names.
21 :type q: str
22 :return: A list of Tag objects associated with the specified user and optionally
23 filtered by the search query.
24 :rtype: list[Tag]
25 """
26 query = db.session.query(Tag).filter(Tag.owner_id == user_id)
27 if q:
28 query = query.filter(Tag.name.ilike(f'%{q}%'))
29 query = query.order_by(db.func.lower(Tag.name))
30 return query.all()
33def get_or_create_tag(user_id, tag_name) -> Tag:
34 """
35 Find an existing tag for this user or add a new one if it doesn't exist.
36 :param user_id:
37 :param tag_name:
38 :return:
39 """
40 # First, sanitize the tag name to remove any HTML
41 tag_name = bleach.clean(tag_name, tags=[], strip=True)
43 # Then apply other validations
44 if not tag_name or len(tag_name) > 32:
45 raise ValueError("Tag name must be between 1 and 32 characters")
47 # Validate tag name - allow only alphanumeric characters, hyphens, and spaces
48 if not tag_name or not re.match(r'^[a-zA-Z0-9\s\-]+$', tag_name):
49 raise ValueError("Tag names can only contain letters, numbers, spaces, and hyphens")
51 tag_name = tag_name.lower()
52 tag = db.session.query(Tag).filter(Tag.owner_id == user_id, Tag.name == tag_name).first()
53 if tag:
54 return tag
56 tag = Tag(name=tag_name, owner_id=user_id, color=choose_random_color())
57 db.session.add(tag)
58 db.session.commit()
59 return tag
62def tag_book(tag_id, book_id, user_id) -> list[dict]:
63 """
64 Tags a book with a specific tag and returns the updated list of tags for the book.
66 This function associates a given tag with a book by adding an entry to the TagBook
67 table and committing it to the database. After successfully tagging the book, it
68 retrieves and returns the updated list of tags associated with the book, including
69 their names and colors, sorted by their IDs.
71 :param tag_id: The ID of the tag to be associated with the book.
72 :type tag_id: int
73 :param book_id: The ID of the book that will be tagged.
74 :type book_id: int
75 :param user_id: The ID of the user performing the tagging operation. Only tags
76 owned by this user will be included in the result.
77 :type user_id: int
78 :return: A list of dictionaries where each dictionary represents a tag associated
79 with the book. Each dictionary contains the tag name and its corresponding color.
80 :rtype: list[dict]
81 """
82 # Does the book already have this tag?
83 book_tag = (db.session.query(TagBook)
84 .filter(TagBook.tag_id == tag_id, TagBook.book_id == book_id)
85 .first())
86 if not book_tag:
87 book_tag = TagBook(tag_id=tag_id, book_id=book_id)
88 db.session.add(book_tag)
89 db.session.commit()
91 # return a new set of tags for the book, sorted in TagBook.id order
92 return get_tags_and_colors(book_id, user_id)
95def get_tags_for_user_with_colors(user_id) -> list[dict]:
96 """
97 Retrieve a list of tags and colors defined by a specific user.
98 """
99 tags = (db.session.query(Tag)
100 .filter(Tag.owner_id == user_id)
101 .order_by(Tag.id)
102 .all())
103 # return a list of tags
104 tag_and_colors = [{'value': tag.name, 'color': tag.color} for tag in tags]
105 return tag_and_colors
108def get_tags_and_colors(book_id, user_id):
109 """
110 Retrieve a list of tags and colors associated with a specific book for a specific user.
112 This function queries the database for tags linked to a book, filters the tags
113 to ensure they belong to a specific user, and then assembles a list of tags with
114 their associated color details. The result includes all tags ordered by their
115 tag-book association records.
117 :param book_id: The unique identifier of the book to retrieve tags for.
118 :type book_id: int
119 :param user_id: The unique identifier of the user whose tags are to be retrieved.
120 :type user_id: int
121 :return: A list of dictionaries, where each dictionary contains the tag name and its color
122 in the format {'tag': tag_name, 'color': tag_color}.
123 :rtype: list[dict]
124 """
125 tag_books = (db.session.query(TagBook)
126 .options(db.joinedload(TagBook.tag))
127 .join(Tag, TagBook.tag_id == Tag.id)
128 .filter(
129 TagBook.book_id == book_id,
130 Tag.owner_id == user_id)
131 .order_by(TagBook.id)
132 .all())
133 # return a list of tag and color objects for the book
134 tag_and_colors = [{'value': tb.tag.name, 'color': tb.tag.color} for tb in tag_books]
135 return tag_and_colors
138def find_tag_for_user(tag_name, user_id) -> Tag or None:
139 """
140 Finds a tag for a specific user based on the tag's name and the user's ID.
142 This function queries the database to find and return the tag that matches
143 the given name and belongs to the specified user. If no matching tag is
144 found, the function returns None.
146 :param tag_name: The name of the tag to search for.
147 :type tag_name: str
148 :param user_id: The unique identifier of the user who owns the tag.
149 :type user_id: int
150 :return: The matching tag instance if found, or None if no match exists.
151 :rtype: Tag or None
152 """
153 return db.session.query(Tag).filter(Tag.owner_id == user_id, Tag.name == tag_name).first()
156def remove_tag_from_book(tag_id, book_id, user_id):
157 """
158 Removes a tag from a book and returns the updated list of tags associated with the book
159 sorted by the TagBook.id. This function performs a deletion operation for the given
160 tag-book association and commits the change to the database. Afterward, it retrieves
161 and returns the updated set of tags for the book.
163 :param tag_id: The unique identifier of the tag to be removed.
164 :param book_id: The unique identifier of the book from which the tag will be removed.
165 :param user_id: The unique identifier of the user associated with the operation.
166 :return: The updated set of tags for the specified book, including their associated
167 colors sorted by their TagBook.id.
168 """
169 db.session.query(TagBook).filter(TagBook.tag_id == tag_id, TagBook.book_id == book_id).delete()
170 db.session.commit()
172 # return a new set of tags for the book, sorted in TagBook.id order
173 return get_tags_and_colors(book_id, user_id)