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

1""" 

2 Database service routines associated with tags. 

3""" 

4import re 

5 

6import bleach 

7 

8from app import db 

9from app.helpers.tag_colors import choose_random_color 

10from app.models import Tag, TagBook 

11 

12 

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. 

17 

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() 

31 

32 

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) 

42 

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") 

46 

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") 

50 

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 

55 

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 

60 

61 

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. 

65 

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. 

70 

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() 

90 

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) 

93 

94 

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 

106 

107 

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. 

111 

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. 

116 

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 

136 

137 

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. 

141 

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. 

145 

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() 

154 

155 

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. 

162 

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() 

171 

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)